как нарисовать плавную кривую через N точек с помощью холста javascript HTML5?

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

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

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

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


person Homan    schedule 14.08.2011    source источник
comment
Что вы имеете в виду под плавным? Бесконечно дифференцируемый? Дважды дифференцируемые? Кубические сплайны (кривые Безье) обладают множеством хороших свойств, дважды дифференцируемы и достаточно просты для вычисления.   -  person Kerrek SB    schedule 14.08.2011
comment
@Kerrek SB, под гладким я имею в виду, что визуально не может обнаружить никаких углов / выступов и т. Д.   -  person Homan    schedule 14.08.2011
comment
@sketchfemme, вы визуализируете линии в реальном времени или откладываете визуализацию до тех пор, пока не наберете кучу точек?   -  person Crashalot    schedule 28.03.2012
comment
@Crashalot Я собираю точки в массив. Для использования этого алгоритма необходимо как минимум 4 балла. После этого вы можете выполнять рендеринг в реальном времени на холсте, очищая экран при каждом вызове mouseMove.   -  person Homan    schedule 23.04.2012
comment
@sketchfemme: Не забудьте принять ответ. Это нормально, если это ваш собственный.   -  person T.J. Crowder    schedule 27.09.2013
comment
вам нужно проверить fit-curve.js .. ... а вот демонстрационная страница.   -  person ashleedawg    schedule 08.07.2021


Ответы (13)


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

Это решение было извлечено из книги «Анимация Foundation 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);
person Homan    schedule 14.08.2011
comment
+1 Это отлично сработало для проекта JavaScript / холста, над которым я работаю - person Matt; 09.01.2012
comment
Рад помочь. К вашему сведению, я запустил панель для рисования холста html5 с открытым исходным кодом, которая представляет собой плагин jQuery. Это должно быть полезной отправной точкой. github.com/homanchou/sketchyPad - person Homan; 23.02.2012
comment
Это хорошо, но как сделать кривую, чтобы она проходила через все точки? - person Richard; 01.09.2012
comment
При использовании этого алгоритма каждая последующая кривая должна начинаться с конечной точки предыдущей кривой? - person Lee Brindley; 04.12.2013
comment
Спасибо большое, Хоман! Оно работает! Я потратил столько дней, чтобы решить эту проблему. И привет от сообщества Delphi Android / iOS! - person alitrun; 18.06.2018

Немного поздно, но для протокола.

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

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

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>

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

Пример пикселя

Вы можете легко расширить холст и называть его так:

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.

person Community    schedule 20.03.2013
comment
Во-первых: это великолепно. :-) Но глядя на это изображение, не создается ли (вводящее в заблуждение) впечатление, что значения на самом деле упали ниже значения №10 на пути между №9 и №10? (Я считаю, исходя из реальных точек, которые я вижу, поэтому №1 будет точка около вершины начальной нисходящей траектории, №2 - точка в самом низу [самая нижняя точка на графике] и так далее ... ) - person T.J. Crowder; 27.09.2013
comment
@ T.J.Crowder, что правильное поведение при интерполяции кривой. Вы можете отрегулировать это, отрегулировав значение натяжения. На натяжение влияют обе точки, предыдущая и следующая, и из-за крутого угла вверх для следующей точки предыдущая точка вынуждена округляться раньше. Так работает кардинальный сплайн :-) - person ; 27.09.2013
comment
Спасибо. Но опять же, не должен ли график не вводить в заблуждение? Возможно, сделав цель сплайна точкой немного выше на восходящем колебании, чтобы самая низкая точка была точкой, отображаемой на графике? Я просто вижу, как непрофессионалы (такие как я, где задействовано сложное построение графиков) очень легко неверно истолковывают это. - person T.J. Crowder; 27.09.2013
comment
@ T.J. Crowder, извини, я не совсем понимаю, что ты имеешь в виду. График представляет собой фактический снимок заданных и визуализированных точек (из демонстрации, поставляемой с функцией). Не знаю, как это может вводить в заблуждение ... Это не график, это кардинальный сплайн. Я что-то упускаю? - person ; 27.09.2013
comment
@ Кен: Или я. :-) Что касается нумерации, которую я перечислил ранее, есть ли данные за провалом на графике до # 10 или нет? - person T.J. Crowder; 28.09.2013
comment
Просто хочу сказать, что после нескольких дней поиска это была единственная утилита, которая действительно работала в точности так, как я хотел. Огромное спасибо - person cnp; 10.02.2014
comment
К сожалению, статья, которую вы пытаетесь просмотреть, была удалена 18 января 2015 года. Перейдите в Оглавление сценариев на стороне клиента, чтобы просмотреть список доступных статей в этом разделе. - person Domi; 19.01.2015
comment
ДА ДА ДА Спасибо! Я вскочил и танцевал от радости. - person Jeffrey Sun; 05.05.2015
comment
@ T.J. Crowder (извините за небольшое (?!) Опоздание :)) Провал является результатом расчета натяжения. Чтобы попасть в следующую точку под правильным углом / направлением, натяжение заставляет кривую опускаться, чтобы она могла продолжаться под правильным углом для следующего сегмента (угол, вероятно, здесь не очень хорошее слово, моему английскому не хватает ...) . Натяжение рассчитывается с использованием двух предыдущих и двух следующих точек. Вкратце: нет, он не представляет никаких реальных данных, а просто расчет натяжения. - person ; 27.05.2015
comment
В вашем коде есть ошибка типа. Параметр ptsa должен быть pts, иначе он выдаст ошибки. - person gfaceless; 23.08.2015
comment
Я использую холст html 5, но моя анимация не работает. Не могли бы вы помочь мне с этим вопросом. просьба: stackoverflow.com/questions/37208156/ - person Learning-Overthinker-Confused; 14.05.2016
comment
Как он масштабируется, если я хорошо понимаю, что он генерирует много промежуточных точек, и вы присоединитесь к ним, это может легко стать огромным - person caub; 19.08.2016
comment
@ K3N: Мой Javascript действительно заржавел, и я не трогал его уже много лет. Но для определенного проекта мне это может понадобиться. Я скопировал ваш код в этот HTML-файл и при открытии в Chrome (v59.0.3071.115 , 64-бит), я видел только пустой ящик. Никакой кривой не нарисовано. - person anta40; 13.07.2017
comment
@ anta40 не забудьте запустить скрипт после загрузки DOM, т.е. либо в window.onload = ..., либо поместить скрипт в конец тела. - person ; 15.07.2017
comment
Имейте в виду, что эта функция дает нежелательные результаты на графиках с неравномерным интервалом между выборками. кривая может выходить за пределы оси x и изгибаться назад во времени к следующей точке и через нее, а также изгибаться назад, образуя S-образные формы на графике. Также интерполяция может опускаться ниже нуля, что на некоторых графиках нежелательно (кстати, это легко исправить). - person Bas Goossen; 12.02.2018
comment
Давным-давно вы опубликовали это решение, и сегодня вы помогли мне решить большую проблему. Большое спасибо! - person ÂlexBay; 28.08.2018
comment
@ ÂlexBay рад, что это сработало (и спасибо за отзывы, мы очень ценим это). Кстати, у меня есть обновленная версия этого кода, которую можно найти здесь. - person ; 31.08.2018
comment
Произошла небольшая ошибка с получением первой и последней точек и добавлением. Для первого добавления с начала массива вам нужно выполнить одно и то же добавление дважды, получив координаты x и y. В противном случае вы дважды копируете координату x - person SuperSecretAndHiddenFromWork; 18.12.2018
comment
Есть ли способ получить точку на этом пути? Скажем, я хотел бы получить x, y на 30% пути. - person arpo; 11.10.2020
comment
Чтобы получить правильное сглаживание замкнутых кривых, последнюю и предпоследнюю точки нужно скопировать в начало (не копируйте последнюю точку дважды). Как это: stackoverflow.com/review/suggested-edits/23234666 - person nocnokneo; 04.02.2021

Первый ответ не пройдет по всем пунктам. Этот график точно пройдет через все точки и будет идеальной кривой с точками как [{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);
}
person Abhishek Kanthed    schedule 05.12.2016
comment
Это, безусловно, самый простой и правильный подход. - person haymez; 27.06.2017

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

person Daniel Patru    schedule 01.12.2013

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

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

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

Вот сравнение до и после:

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

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

person Eric K.    schedule 18.09.2016

Я обнаружил, что это прекрасно работает

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();
}
person Roy Aarts    schedule 19.03.2018

Попробуйте 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/

person Eric Rowell    schedule 18.01.2013
comment
Удивительная библиотека! Лучшая для задачи! - person Dziad Borowy; 08.10.2013
comment
да!! Мне нужна была функция blob (), чтобы создать замкнутую форму, проходящую через все точки. - person AwokeKnowing; 11.01.2014
comment
404 Страница не найдена. - person dieter; 08.02.2016
comment

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

Этот код мне идеально подходит:

this.context.beginPath();
this.context.moveTo(data[0].x, data[0].y);
for (let i = 1; i < data.length; i++) {
  this.context.bezierCurveTo(
    data[i - 1].x + (data[i].x - data[i - 1].x) / 2,
    data[i - 1].y,
    data[i - 1].x + (data[i].x - data[i - 1].x) / 2,
    data[i].y,
    data[i].x,
    data[i].y);
}

у вас правильная плавная линия и правильные конечные точки ВНИМАНИЕ! (y = высота холста - y);

person beka shkubuliani    schedule 24.08.2020

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

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

person James Pearce    schedule 17.07.2016

Если вы хотите определить уравнение кривой через n точек, следующий код предоставит вам коэффициенты полинома степени n-1 и сохранит эти коэффициенты в массиве coefficients[] (начиная с постоянного члена). Координаты x не обязательно должны быть в порядке. Это пример полинома Лагранжа.

var xPoints=[2,4,3,6,7,10]; //example coordinates
var yPoints=[2,5,-2,0,2,8];
var coefficients=[];
for (var m=0; m<xPoints.length; m++) coefficients[m]=0;
    for (var m=0; m<xPoints.length; m++) {
        var newCoefficients=[];
        for (var nc=0; nc<xPoints.length; nc++) newCoefficients[nc]=0;
        if (m>0) {
            newCoefficients[0]=-xPoints[0]/(xPoints[m]-xPoints[0]);
            newCoefficients[1]=1/(xPoints[m]-xPoints[0]);
    } else {
        newCoefficients[0]=-xPoints[1]/(xPoints[m]-xPoints[1]);
        newCoefficients[1]=1/(xPoints[m]-xPoints[1]);
    }
    var startIndex=1; 
    if (m==0) startIndex=2; 
    for (var n=startIndex; n<xPoints.length; n++) {
        if (m==n) continue;
        for (var nc=xPoints.length-1; nc>=1; nc--) {
        newCoefficients[nc]=newCoefficients[nc]*(-xPoints[n]/(xPoints[m]-xPoints[n]))+newCoefficients[nc-1]/(xPoints[m]-xPoints[n]);
        }
        newCoefficients[0]=newCoefficients[0]*(-xPoints[n]/(xPoints[m]-xPoints[n]));
    }    
    for (var nc=0; nc<xPoints.length; nc++) coefficients[nc]+=yPoints[m]*newCoefficients[nc];
}
person Kevin Bertman    schedule 15.04.2020

Несколько иной ответ на исходный вопрос;

Если кто-то хочет нарисовать фигуру:

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

Тогда, надеюсь, моя функция ниже может помочь

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

person midanosi    schedule 24.03.2021

Bonjour

Я ценю решение user1693593: полиномы Эрмита кажутся лучшим способом контролировать то, что будет нарисовано, и наиболее удовлетворительным с математической точки зрения. Тема вроде бы давно закрыта, но, может быть, некоторым опоздавшим, вроде меня, она все еще интересна. Я искал бесплатный интерактивный конструктор сюжетов, который позволил бы мне хранить кривую и повторно использовать ее где-нибудь еще, но не нашел ничего подобного в Интернете: поэтому я сделал это по-своему, из источника в Википедии. упомянул user1693593. Здесь сложно объяснить, как это работает, и лучший способ узнать, стоит ли оно того, - взглянуть на https://sites.google.com/view/divertissements/accueil/splines.

person Jean Fontaine    schedule 20.04.2021