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

172
对于一个绘图应用程序,我将鼠标移动坐标保存到数组中,然后使用lineTo进行绘制。 产生的线条不够平滑。如何在所有收集到的点之间产生一个单一的曲线?
我搜索了谷歌,但只找到三种画线的函数:对于2个样本点,简单地使用lineTo;对于3个样本点,使用quadraticCurveTo;对于4个样本点,使用bezierCurveTo
(我尝试在数组中每4个点绘制一个bezierCurveTo,但这会导致四个采样点时出现折痕,而不是连续平滑的曲线)。
如何编写一个功能以绘制5个采样点及更多的平滑曲线?

5
“smooth”指的是什么?无限可微分?二阶可导?立方样条曲线(“贝塞尔曲线”)具有许多良好的特性,且二阶可导,计算也相对简单。 - Kerrek SB
10
“smooth”在此指外观上无法检测到任何角落/尖点等。 - Homan
@sketchfemme,你是实时渲染线条,还是等收集一堆点后再渲染? - Crashalot
@Crashalot 我正在将点收集到一个数组中。你需要至少4个点才能使用这个算法。之后,你可以在每次mouseMove调用时清除屏幕,在画布上实时渲染。 - Homan
2
@sketchfemme:不要忘记接受一个答案。如果是你自己的也没关系 - T.J. Crowder
你一定要看看 fit-curve.js ... 这里还有一个 演示页面 - ashleedawg
14个回答

153
使用不连续的“curveTo”类型函数将后续样本点连接起来的问题在于,这些曲线相遇的地方并不平滑。这是因为这两条曲线共享一个端点,但受到完全不同的控制点的影响。解决方法之一是“curve to”与下两个后续采样点之间的中点。使用这些新的插值点连接曲线可以在端点处产生平稳过渡(对于一次迭代而言,什么是端点变成了下一次迭代的控制点)。换句话说,这两条不连续的曲线现在有很多共同点。

这个解决方案来自书籍《Foundation ActionScript 3.0 Animation: Making things move》第95页-渲染技术:创建多条曲线。

注意:这个解决方案实际上并没有通过每个点进行绘制,这也是我的问题标题(它通过样本点近似曲线但从未经过样本点),但对于我的目的(绘图应用程序)来说,这已经足够好了,而且在视觉上你看不出区别。确实有一种方法可以通过所有样本点,但它更加复杂(请参见http://www.cartogrammar.com/blog/actionscript-curves-update/)。
以下是近似方法的绘图代码:
// move to the first point
   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);
   }
 // curve through the last two points
 ctx.quadraticCurveTo(points[i].x, points[i].y, points[i+1].x,points[i+1].y);

作为一个可运行的片段:

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const points = [
  {x: 50, y: 50},
  {x: 180, y: 100},
  {x: 75, y: 120},
  {x: 40, y: 40},
];

// move to the first point
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);
}

// curve through the last two points
ctx.quadraticCurveTo(
  points[i].x,
  points[i].y,
  points[i + 1].x,
  points[i + 1].y
);
ctx.stroke();
<canvas width="600" height="600"></canvas>


+1 这对我正在进行的 JavaScript/canvas 项目非常有效。 - Matt
1
很高兴能够帮忙。顺便说一下,我已经开始了一个开源的HTML5画布绘图板,它是一个jQuery插件。这应该是一个有用的起点。https://github.com/homanchou/sketchyPad - Homan
5
好的,但是您如何使曲线经过所有点? - Richard
这个算法中,每条连续的曲线是否应该从前一条曲线的末端开始? - Lee Brindley
非常感谢你,Homan!它起作用了!我花了很多天来解决它。来自Delphi Android/iOS社区的问候! - alitrun

142
有点晚了,但为了记录。
您可以通过使用cardinal splines(也称为canonical spline)来绘制经过点的平滑曲线,从而实现平滑的线条。
我为画布制作了这个函数 - 它被分成三个函数以增加多样性。主要的包装函数如下所示:
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();
}

要绘制一条曲线,需要按照顺序在数组中包含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]);
}

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>

这导致了以下结果:

Example pix

你可以轻松地扩展画布,这样你就可以像这样调用它:
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 上找到。

7
首先,这太棒了。 :-) 但是看着这个图像,它是否给人一种(误导性的)印象,即数值实际上在从#9到#10之间的路上降至低于值#10?(我按照我能看到的实际点进行计数,因此#1将是初始向下轨迹顶部附近的那个,#2将是图表中最低点[最低点]处的那个,依此类推...) - T.J. Crowder
9
只想说在搜索了几天后,这是唯一一个完全按照我的要求工作的工具。非常感谢。 - cnp
4
是的!谢谢你!我高高跃起,欣喜地跳舞。 - Jeffrey Sun
2
@T.J.Crowder(抱歉有点晚了!)下降是张力计算的结果。为了以正确的角度/方向“击中”下一个点,张力迫使曲线向下走,以便在下一段以正确的角度继续(这里可能不是一个好词,我的英语不太好...)。张力使用前两个和后两个点进行计算。所以简而言之:不,它不代表任何实际数据,只是用于张力计算。 - user1693593
3
很久以前您发布了这个解决方案,今天您帮助我解决了一个重要问题。非常感谢! - ÂlexBay
显示剩余16条评论

35

第一个答案将无法通过所有点。这个图形将准确地通过所有点,成为一个完美的曲线,其点为[{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);
}

2
这绝对是最简单和正确的方法。 - haymez
除了.getContext('2d')之外,我还需要什么?它对我没有画任何东西。 - étale-cohomology
1
在 @étale-cohomology 循环后添加 ctx.stroke() - L.Lauenburg

19
我决定添加内容,而不是将我的解决方案发布到另一个帖子中。 以下是我建立的解决方案,可能不完美,但目前的输出效果很好。
重要提示:它将通过所有点!
如果您有任何想法,让它更好,请与我分享。谢谢。
以下是之前和之后的比较:

enter image description here

将此代码保存为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>


在第32行,使用dx2 = (nexP.x - preP.x) * -f似乎更符合逻辑,而不是dx2 = (nexP.x - curP.x) * -f,但是结果似乎不够平滑,除非我降低f值(0.2似乎是一个不错的默认值)。 - Grant
@Grant 谢谢,我测试了 dx2 = (nexP.x - preP.x) * -f 这行代码,它会使得线条比实际数据高/低得多,看起来不太自然。 - Eric K.

15

正如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>


13

我发现这个很好用

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

6

惊人的库!这是最适合此任务的库! - Dziad Borowy
是的!我需要使用blob()函数来创建一个经过所有点的封闭形状。 - AwokeKnowing
7
  1. 页面未找到。
- dieter
原始链接 - 404未找到 - 请参见https://web.archive.org/web/20141204030628/http://www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-spline-tutorial/ - satels

3

Bonjour

我欣赏用户1693593提出的解决方案:Hermite多项式似乎是控制绘制图形的最佳方式,也是从数学角度来看最令人满意的方法。这个主题似乎已经封闭了很长时间,但可能还有像我这样的后来者对它感兴趣。 我曾经寻找过一个免费的交互式绘图工具,可以让我存储曲线并在任何其他地方重复使用它,但是在网络上没有找到这种工具:所以我按照用户1693593提供的维基百科来源自己做了一个。 在这里很难解释它的工作原理,如果想知道它是否值得一试,最好的方法就是查看https://sites.google.com/view/divertissements/accueil/splines.


2

虽然有些晚了,但受到Homan的简明回答的启发,我想发表一个更一般化的解决方案(一般化是指Homan的解决方案在点数少于3个的数组上会崩溃):

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

2

对于原始问题的稍有不同答案;

如果有人想要绘制一个形状:

  • 由一系列点描述
  • 线在这些点处有小曲线
  • 线不一定要通过这些点(即略微“内部”通过)

那么希望下面我的函数可以帮助到您。

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

    const drawPointsWithCurvedCorners = (points, ctx) => {
        for (let n = 0; n <= points.length - 1; n++) {
            let pointA = points[n];
            let pointB = points[(n + 1) % points.length];
            let pointC = points[(n + 2) % points.length];

            const midPointAB = {
                x: pointA.x + (pointB.x - pointA.x) / 2,
                y: pointA.y + (pointB.y - pointA.y) / 2,
            };
            const midPointBC = {
                x: pointB.x + (pointC.x - pointB.x) / 2,
                y: pointB.y + (pointC.y - pointB.y) / 2,
            };
            ctx.moveTo(midPointAB.x, midPointAB.y);
            ctx.arcTo(
                pointB.x,
                pointB.y,
                midPointBC.x,
                midPointBC.y,
                radii[pointB.r]
            );
            ctx.lineTo(midPointBC.x, midPointBC.y);
        }
    };

    const shapeWidth = 200;
    const shapeHeight = 150;

    const topInsetDepth = 35;
    const topInsetSideWidth = 20;
    const topInsetHorizOffset = shapeWidth * 0.25;

    const radii = {
        small: 15,
        large: 30,
    };

    const points = [
        {
            // TOP-LEFT
            x: 0,
            y: 0,
            r: "large",
        },
        {
            x: topInsetHorizOffset,
            y: 0,
            r: "small",
        },
        {
            x: topInsetHorizOffset + topInsetSideWidth,
            y: topInsetDepth,
            r: "small",
        },
        {
            x: shapeWidth - (topInsetHorizOffset + topInsetSideWidth),
            y: topInsetDepth,
            r: "small",
        },
        {
            x: shapeWidth - topInsetHorizOffset,
            y: 0,
            r: "small",
        },
        {
            // TOP-RIGHT
            x: shapeWidth,
            y: 0,
            r: "large",
        },
        {
            // BOTTOM-RIGHT
            x: shapeWidth,
            y: shapeHeight,
            r: "large",
        },
        {
            // BOTTOM-LEFT
            x: 0,
            y: shapeHeight,
            r: "large",
        },
    ];

    // ACTUAL DRAWING OF POINTS
    ctx.beginPath();
    drawPointsWithCurvedCorners(points, ctx);
    ctx.stroke();
</script>
</body>

</html>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接