Как рисовать гладкую кривую через N точек с помощью javascript HTML5 canvas?

Для приложения рисования я сохраняю координаты перемещения мыши в массив, а затем рисую их с помощью lineTo. Полученная строка не является гладкой. Как я могу создать одну кривую между всеми собранными точками?

У меня есть googled, но я нашел только 3 функции для рисования линий: для 2 точек выборки просто используйте lineTo. Для трех точек выборки quadraticCurveTo, для 4 точек выборки, bezierCurveTo.

(Я попытался рисовать bezierCurveTo для каждых 4 точек в массиве, но это приводит к перегибу каждые 4 точки выборки вместо непрерывной гладкой кривой.)

Как написать функцию, чтобы нарисовать гладкую кривую с 5 точками выборки и дальше?

Ответ 1

Проблема с присоединением последующих точек выборки вместе с непересекающимися функциями типа "curveTo" заключается в том, что там, где кривые встречаются, не являются гладкими. Это связано с тем, что две кривые имеют конечную точку, но на них влияют полностью непересекающиеся контрольные точки. Одним из решений является "кривая" между серединами между следующими двумя последующими точками выборки. Объединение кривых с использованием этих новых интерполированных точек дает плавный переход в конечных точках (то, что является конечной точкой для одной итерации, становится контрольной точкой для следующей итерации). Другими словами, две несвязанные кривые имеют гораздо больше общего.

Это решение было извлечено из книги "Фундаментальная анимация ActionScript 3.0: перемещение вещей". 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);

Ответ 2

Немного поздно, но для записи.

Вы можете добиться плавных линий, используя кардинальные сплайны (ака канонический сплайн), чтобы нарисовать гладкие кривые, проходящие через точки.

Я сделал эту функцию для холста - он разделился на три функции, чтобы увеличить универсальность. Основная функция обертки выглядит следующим образом:

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

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>

Ответ 3

Первый ответ не пройдет через все точки. Этот граф будет точно проходить через все точки и будет кривой префекта с точками в виде точек как [{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_y1 = (y_mid + points[i].y) / 2;
  var cp_x2 = (x_mid + points[i+1].x) / 2;
  var cp_y2 = (y_mid + points[i+1].y) / 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);
}

Ответ 4

Как Даниэль Говард указывает, Роб Спенсер описывает, что вы хотите в 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 <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>

Ответ 6

Я решил добавить, вместо того, чтобы отправлять мое решение на другое сообщение. Ниже будет решение, которое я построю, может быть не идеальным, но пока что результат хорош.

Важно: он пройдет через все точки!

Если у вас есть идея, чтобы сделать ее лучше, пожалуйста, поделитесь со мной. Спасибо.

Вот сравнение предыдущего:

введите описание изображения здесь

Сохраните этот код в 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>

Ответ 7

Я нашел, что это хорошо работает

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

Ответ 8

Чтобы добавить к методу кардинальных сплайнов K3N и, возможно, обратиться к Т. Дж. Кроудеру, касающемуся опускания кривых в ложных местах, я вставил следующий код в функцию getCurvePoints(), перед 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;
}

Это эффективно создает (невидимый) ограничивающий прямоугольник между каждой парой последовательных точек и обеспечивает, чтобы кривая оставалась в пределах этого ограничивающего прямоугольника - т.е. если точка на кривой выше/ниже/слева/справа от обеих точек, она изменяет свое положение в пределах поля. Здесь используется средняя точка, но это может быть улучшено, возможно, с использованием линейной интерполяции.

Ответ 9

Невероятно поздно, но вдохновленный замечательным простым ответом Хомана, позвольте мне опубликовать более общее решение (общее в том смысле, что решение Homan падает на массивы точек с менее чем тремя вершинами):

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