【问题标题】:Generating a path between two sets of pixel coordinates (x, y)在两组像素坐标 (x, y) 之间生成路径
【发布时间】:2020-02-01 18:52:52
【问题描述】:

我有两组 xy 坐标,开始和结束。起点是我想离开的地方,终点是目的地。

目标是在两个坐标之间生成一个 xy 对象数组,可以对其进行迭代,以生成通往目的地的平滑、非跳跃路径,如下所示。

我已经阅读了有关贝塞尔曲线的内容,但我正在努力将实现可视化并想知道是否有更简单的方法来解决上述问题?

【问题讨论】:

  • 你想让曲线有某种斜率吗?如果不解决y = mx + b 就足够了
  • 在这种情况下需要某种形式的斜率,但如果不是,y = mx + b 绝对足够了。
  • 路边的定义是什么?如果你只有两个点,就没有办法应用贝塞尔或其他样条机制,得到一条非直线。您至少需要一个参数,例如第三个点。但即使只有 3 个点,您也可以找到由这三个点定义的圆,并生成它的圆弧。为了更多的弯曲,你需要第四个点,...
  • 如果你想要一个斜坡,我会按照@trincot 说的去做。选择与前两个不共线的第三个点,然后选择您最喜欢的回归技术来获得直线
  • 最终目标是沿着曲线路径(用于模拟目的)平滑对象移动,该路径是从开始和结束边界动态生成的。该算法所需的唯一输出是用于循环移动的点数组(这已经使用模拟数据处理) - 不需要绘图或抗锯齿。我现在知道我需要根据这些随机边界随机生成一些控制点,最好是根据点之间的距离来实现真实感。

标签: javascript algorithm path coordinates bezier


【解决方案1】:

对于贝塞尔曲线,我采用了 Maxim Shemanarev(参见 https://web.archive.org/web/20190307062751/http://antigrain.com:80/research/adaptive_bezier/ )的算法,该算法涉及建立一个容差,通过该容差将曲线递归地分解为线性段。通过使用容差,贝塞尔曲线较平坦的部分会产生很少的线段,而对于贝塞尔曲线的急剧弯曲,为了正确描绘曲线,需要增加线段的数量。

Maxim Shemanarev 的算法使用端点(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 ) {

  let tolerance = bezierTolerance * bezierTolerance;
  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 ) < tolerance &&
        distanceToSegmentSqr( p1, p4, p3 ) < tolerance &&
        distanceToSegmentSqr( p1, p4, p1234 ) < tolerance )
    {
      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( 5, { x:135, y: 45 }, { x: 165, y: 45 }, { x: 160, y: 110 }, { x:190, y:110 }, false, true, true );

draw( 0.25, { 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 = 5。同样,从 P1 和 P4 形成的线段到每个点 P2、P3 和 P1234 的距离都小于 5 的任何贝塞尔细分都将符合充分“弯曲”的条件,并作为线段添加到结果。

作为一个极端的例子,底部的曲线包括一个非常陡峭的弯曲。通过设置bezierTolerance = 0.25,您会注意到该算法通过包含额外的细分以更好地表示曲线来优雅地处理急弯...

简而言之,高容差会在绘制时产生较少的线段和不太理想的贝塞尔曲线,而低容差会产生更多的线段和更好看的贝塞尔曲线。但是,公差方式太小会产生不必要数量的线段,因此需要进行一些实验来建立一个平衡良好的bezierTolerance...

【讨论】:

    【解决方案2】:

    三次贝塞尔曲线的数学运算可以归结为一个方程 (source):

    该等式在伪代码中的实现如下所示:

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

    这是一个示例输出,其中控制点的位置可以给出平缓的曲线:

    起点是黑点,第一个控制点在黑线的终点。终点为绿色,第二个控制点在绿线的末端。请注意,控制点确定远离起点/终点的初始方向。从起点/终点到相应控制点的距离可以认为是初始速度。使用中等速度会在曲线上产生大致均匀分布的点(如上图所示)。

    使用快或慢的速度会导致曲线上的点不均匀。例如,在下图中,黑色的初速较高,绿色的初速较低,导致点在绿色点附近聚集。

    如果两个速度都很快,那么点就会集中在中间。如果两个速度都很慢,则这些点在开始/结束时会聚在一起,并在中间散开。所以有一个初始速度相等的最佳位置,并且恰好保持点均匀分布。

    【讨论】:

      【解决方案3】:

      你有两组点,所以一条直线可以适合它。在这种情况下,您可以使用直线方程: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 值递增 1 的坐标,但增量也可以是任何分数。

      【讨论】:

      • 很高兴看到可视化 - 但是,不幸的是,理想情况下需要某种形式的控制点来调整到目标点的路径。这是我目前最难实施的。
      • 我没明白你的意思。你想让这条线变成曲线吗?
      • 是的,正是:)
      【解决方案4】:

      /*
      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);

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2023-03-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-11-30
        • 1970-01-01
        • 1970-01-01
        • 2020-04-22
        相关资源
        最近更新 更多