Генерация пути между двумя наборами координат пикселей (x, y) - PullRequest
1 голос
/ 01 февраля 2020

У меня есть два набора координат xy, начало и конец. Начало - это то место, откуда я хотел бы перейти, а конец - место назначения.

Цель состоит в том, чтобы создать массив объектов xy между двумя координатами, которые можно повторять для получения гладкого, не - простой путь к месту назначения, как показано ниже.

enter image description here

Я закончил чтение вокруг кривых Безье, но я изо всех сил пытаюсь визуализировать реализацию и хотел бы узнать, есть ли более простой способ решить вышесказанное?

Ответы [ 4 ]

2 голосов
/ 02 февраля 2020

Для кривой Безье я адаптировал алгоритм от Максима Шеманарева (см. https://web.archive.org/web/20190307062751/http: //antigrain.com: 80 / research / adaptive_bezier / ), который включает в себя установление допуска, с помощью которого можно рекурсивно разбить кривую на линейные сегменты. Используя допуск, более плоские части кривой Безье дают очень мало сегментов линии, а при резких изгибах кривой Безье количество сегментов линии увеличивается для правильного отображения кривой.

Алгоритм Максима Шеманарева использовал расстояние между конечными точками (P1 и P4) и контрольными точками Безье (P2 & P3) в качестве средства определения, был ли подразделенный сегмент достаточно в пределах допуска, или нужна ли кривая дальнейшее подразделение.

Я обнаружил, однако, что его алгоритм был излишне сложным, учитывая граничные случаи, когда кривая Безье содержала очень резкую кривую. Моя адаптация, чтобы упростить его алгоритм, включает проверку допуска для расстояния между линией, образованной конечными точками (P1 и P4) с вычисленной средней точкой (P1234). При добавлении этой проверки допуска любой острый изгиб, который все еще существует между конечными точками, вызовет дальнейшее подразделение на меньшие отрезки линии ...

Реализация javascript выглядит следующим образом ...

<!DOCTYPE html>
<html><body>

<canvas id="myCanvas" width="300" height="300" style="border:1px solid #d3d3d3;"></canvas>

<script>

var canvas = document.getElementById("myCanvas");

function distanceSqr(v, w) {
  return (v.x - w.x) ** 2 + (v.y - w.y) ** 2;
};

function distanceToSegmentSqr(v, w, p) {
  var vwLength = distanceSqr(v, w);
  if (vwLength === 0) return distanceSqr(p, v);
  var t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / vwLength;
  t = Math.max(0, Math.min(1, t));
  return distanceSqr(p, { x: v.x + t * (w.x - v.x),	y: v.y + t * (w.y - v.y) });
};

function lineateBezier( bezierTolerance, p1, p2, p3, p4 ) {

  var result = [ p1 ];
  
  function recurse( p1, p2, p3, p4 ) {
    
    var p12 = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };
    var p23 = { x: (p2.x + p3.x) / 2, y: (p2.y + p3.y) / 2 };
    var p34 = { x: (p3.x + p4.x) / 2, y: (p3.y + p4.y) / 2 };
    var p123 = { x: (p12.x + p23.x) / 2, y: (p12.y + p23.y) / 2 };
    var p234 = { x: (p23.x + p34.x) / 2, y: (p23.y + p34.y) / 2 };
    var p1234 = { x: (p123.x + p234.x) / 2, y: (p123.y + p234.y) / 2 };

    if( distanceToSegmentSqr( p1, p4, p2 ) < bezierTolerance &&
        distanceToSegmentSqr( p1, p4, p3 ) < bezierTolerance &&
        distanceToSegmentSqr( p1, p4, p1234 ) < bezierTolerance )
    {
      result.push( p1234 );
    } else {
      recurse( p1, p12, p123, p1234 );
      recurse( p1234, p234, p34, p4 );
    }
  };
  
  recurse (p1, p2 || p1, p3 || p4, p4);
  result.push( p4 );
     
  return result;
};

function draw( bezierTolerance, startEndPoint, startControlPoint, endControlPoint, endPoint, clearCanvasFlag, pointsFlag, controlFlag ) {

  // Get line segment points 
  let lineSegments = lineateBezier( bezierTolerance, startEndPoint, startControlPoint, endControlPoint, endPoint );

  // Clear canvas
  var ctx = canvas.getContext("2d");
  if ( clearCanvasFlag ) {
    ctx.clearRect( 0, 0, canvas.width, canvas.height );
  }

  // Draw line segments 
  ctx.beginPath();
  ctx.moveTo( lineSegments[ 0 ].x, lineSegments[ 0 ].y );
  for ( let i = 1; i < lineSegments.length; i++ ) {
    ctx.lineTo( lineSegments[ i ].x, lineSegments[ i ].y );
  }
  ctx.strokeStyle = '#000000';
  ctx.stroke();
  
  // Draw points
  if ( pointsFlag ) {
    for ( let i = 0; i < lineSegments.length; i++ ) {
      ctx.beginPath();
      ctx.arc( lineSegments[ i ].x, lineSegments[ i ].y, 1.5, 0, 2 * Math.PI );
      ctx.strokeStyle = '#ff0000';
      ctx.stroke();
    }        
  }
  
  // Draw control points...
  if ( controlFlag ) {
    ctx.beginPath();
    ctx.moveTo( startEndPoint.x, startEndPoint.y );
    ctx.lineTo( startControlPoint.x, startControlPoint.y );
    ctx.strokeStyle = '#0000ff';
    ctx.stroke();
    
    ctx.beginPath();
    ctx.moveTo( endPoint.x, endPoint.y );
    ctx.lineTo( endControlPoint.x, endControlPoint.y );
    ctx.stroke();
  }
  
}

draw( 1,  { x:35, y: 45 }, { x: 65, y: 45 }, { x: 60, y: 110 }, { x:90, y:110 }, true, true, true );
draw( 20, { x:135, y: 45 }, { x: 165, y: 45 }, { x: 160, y: 110 }, { x:190, y:110 }, false, true, true );

draw( 0.1, { x:20, y: 200 }, { x: 250, y: 290 }, { x: 250, y: 160 }, { x:20, y:250 }, false, true, true );

</script>

</body></html>

Обратите внимание на критическую переменную bezierTolerance. При выполнении примера выше, верхняя кривая слева использует bezierTolerance = 1, что означает, что, пока расстояние в квадрате между конечными точками (P1 и P4) относительно P2, P3 и P1234 меньше 1, то сегмент является достаточно линейным, и, следовательно, дальнейшее подразделение не происходит.

Для сравнения, верхняя кривая справа использует bezierTolerance = 20. Опять же, любое подразделение Безье, в котором расстояния от отрезка линии, образованного P1 и P4, до каждой из точек P2, P3 и P1234, все меньше, чем sqrt (20) (~ 4.47), будут квалифицироваться как «достаточно прямые», и быть добавлен как линейный сегмент к результатам.

В качестве крайнего примера, кривая внизу включает очень резкий изгиб. Установив bezierTolerance = 0.1, вы заметите, что алгоритм изящно обрабатывает резкий изгиб, добавляя дополнительные подразделения для лучшего представления кривой ...

Короче говоря, высокий допуск даст меньше отрезков и меньше, чем оптимальная кривая Безье при рисовании, а низкий допуск даст больше отрезков и лучшую кривую Безье. Но слишком малый допуск приведет к результату с ненужным количеством отрезков, поэтому необходимо провести эксперименты, чтобы установить sh правильное bezierTolerance ...

1 голос
/ 02 февраля 2020

Математика для куба c Кривая Безье сводится к одному уравнению ( источник ):

enter image description here

Реализация этого уравнения в псевдокоде выглядит следующим образом:

let p1 be the start point
let c1 be the first control point
let c2 be the second control point
let p2 be the end point

for (i = 0; i <= 20; i++)
{
   t = i / 20.0;
   s = 1.0 - t;
   x = s*s*s*p1.x + 3*s*s*t*c1.x + 3*s*t*t*c2.x + t*t*t*p2.x;
   y = s*s*s*p1.y + 3*s*s*t*c1.y + 3*s*t*t*c2.y + t*t*t*p2.y;
   output point(x,y)
}

Вот пример вывода с контрольными точками, расположенными так, чтобы получить плавную кривую:

enter image description here

Начальной точкой является черная точка, а первая контрольная точка находится в конце черной линии. Конечная точка зеленого цвета, а вторая контрольная точка находится в конце зеленой линии. Обратите внимание, что контрольная точка определяет начальное направление от начальной / конечной точки. Расстояние от начальной / конечной точки до соответствующей контрольной точки можно рассматривать как начальную скорость. Использование умеренной скорости приведет к точкам на кривой, которые примерно равномерно распределены (как показано на рисунке выше).

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

enter image description here

Если обе скорости быстрые, точки сгруппированы в середине. Если обе скорости медленные, точки сгруппированы в начале / конце и растянуты в середине. Таким образом, есть приятное место, где начальные скорости равны, и как раз правильно, чтобы точки были равномерно распределены.

0 голосов
/ 02 февраля 2020

/*
you can pass an equation of the form y = a * x^2 + b * x + c (parabola) between the points
the equation has 3 unknowns a, b, and c. to get those apply the conditions: when x = 35, y = 45 (start) and when x = 90, y = 110 (end).
the problem is that you can't solve for 3 unknowns with just 2 equations
to get a third equation assume that at the midpoint, where x = (35 + 90) / 2 = 62.5, y = 85
note: if we were passing a straight line between start and end, the y coordinate of the midpoint would be (45 + 110) / 2 = 77.5
so, anything greater (or less) than 77.5 would be OK
the 3 equations are:
35 * 35 * a + 35 * b + c = 45
90 * 90 * a + 90 * a + c = 110
62.5 * 62.5 * a + 62.5 * b + c = 85
you can use Cramer's rule to get the solution to these equations
to get the 4 determinants needed you can use 
*/
const determinant = arr => arr.length === 1 ? arr[0][0] : arr[0].reduce((sum, v, i) => sum + v * (-1) ** i * determinant(arr.slice(1).map(x => x.filter((_, j) => i !== j))), 0);
0 голосов
/ 01 февраля 2020

У вас есть два набора точек, поэтому в них может поместиться прямая линия. В этом случае вы можете использовать уравнение прямой: y = mx + b; где m - это уклон, а b - точка пересечения y.

const coord1 = [2, 5];
const coord2 = [4, 7];

function generatePath(arr1, arr2) {
    const m = (arr2[1] - arr1[1]) / (arr2[0] - arr1[0]);
    const b = arr1[1] - m*arr1[0];
    let lineArray = [];

    for(let x=arr1[0]; x<arr2[0]; x++) {
        let y = m*x + b;
        lineArray.push([x,y]);
    }

    return lineArray;
}

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

...