Как нарисовать гладкую кривую через N точек, используя JavaScript HTML5 Canvas? - PullRequest
111 голосов
/ 14 августа 2011

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

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

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

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

Ответы [ 9 ]

114 голосов
/ 14 августа 2011

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

Это решение было извлечено из книги "Основы анимации ActionScript 3.0: создание движущихся объектов". стр.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);
89 голосов
/ 20 марта 2013

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

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

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

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>

Это приводит к следующему:

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 .

13 голосов
/ 05 декабря 2016

Первый ответ не пройдет через все пункты. Этот график будет точно проходить через все точки и будет представлять собой кривую префекта с точками в виде точек как [{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);
}
6 голосов
/ 01 декабря 2013

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

Попробуйте KineticJS - вы можете определить сплайн с массивом точек.Вот пример:

Старый URL: http://www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-spline-tutorial/

См. Архивный URL: https://web.archive.org/web/20141204030628/http://www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-spline-tutorial/

4 голосов
/ 18 сентября 2016

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

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

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

Ниже приведено сравнение:

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>
3 голосов
/ 19 марта 2018

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

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 голосов
/ 20 мая 2018

Невероятно поздно, но вдохновленный блестяще простым ответом Хомана, позвольте мне опубликовать более общее решение (общее в том смысле, что решение Хомана дает сбой на массивах точек с менее чем 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);
}
0 голосов
/ 17 июля 2016

Чтобы добавить метод кардинальных сплайнов в 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;
}

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

...