在手绘线/Konva.Line 形状上找到最近的 x 和 y 点,到 canvas 上的点

Find nearest x & y point on a freehand line / Konva.Line shape, to a point on the canvas

我需要找到 Konva.Line 形状上与 canvas 上的任意点最近的点。请参见下面的示例,其中鼠标指针是任意点,彩色线是 Konva.Line。我特别需要一个 Konvajs 实现。

这是一个自答题,请参阅下面我的解决方案。我愿意接受任何更好的建议。

经过一些网络研究,我找到了一种常用的算法来查找路径上的最近点。请参阅 mbostock 的文章。这只需要很少的更改就可以按照我的需要进行操作 - 请参阅下面代码片段中的代码。

这是通过采用 SVG-style 路径定义,使用 get-path-length 函数来实现的(我在这里坚持使用伪命名,因为您的库在确切命名上可能有所不同,请参阅 Konva 版本的代码片段)然后遍历路径上的一堆点,由 get-point-at-length 函数找到,通过简单的数学计算从每个点到任意点的距离。因为这会产生处理成本开销,所以它使用粗略的 step-basis 来获得近似值,然后使用更精细的二进制方法来快速获得最终结果。结果是一个点 - 到给定任意点的路径上最近的点。

所以 - 在 Konva 中启用它...注意目标是一条徒手绘制的线...

第一个问题是,要在 Konva 上下文中的 canvas 上绘制一条徒手线,您需要使用线形。线形状有一个点数组,为沿线的点提供 co-ordinates。你给它点数,Konva 用笔画把点连接起来形成一条线。通过在每个鼠标移动事件中将线推进到鼠标指针位置,可以很容易地创建徒手绘制的线(参见代码片段)。但是,线的点数组没有路径测量函数,因此我们必须将 Konva.Line 转换为 Konva.Path 形状,因为它确实具有我们需要的路径函数。

点到路径的转换很简单。点数组布局为 [x1, y1, x2, y2, ... xn, yn],而路径是布局为“M x1, y1 L x2, y2...L xn, yn”的字符串.它们都可以比这更复杂,但是坚持一条简单的连接点线可以满足这个要求。该代码段包含 pointsToPath() 函数。

现在找到了创建 Konva.Path 形状的路径。

  // use the pointsToPath fn to prepare a path from the line points.
  thePath = pointsToPath(lineShape.points());
  
  // Make a path shape tracking the lineShape because path has more options for measuring.
  pathShape = new Konva.Path({
    stroke: 'cyan',
    strokeWidth: 5,
    data: thePath            
  });
  layer.add(pathShape);

在代码片段中,我用路径形状替换了线形,但甚至可以不将形状添加到 canvas 中,只是将其实例化以用于最近点过程。

所以 - 有了路径,我们可以调用 closestPoint() 函数,为其提供鼠标位置和路径形状,以便该函数可以根据需要调用测量和 point-at-length-getting 函数。

  // showing nearest point -  link mouse pointer to the closest point on the line
  const closestPt = closestPoint(pathShape, {x: mousePos.x, y: mousePos.y});
  connectorLine.points([closestPt.x, closestPt.y, mousePos.x, mousePos.y]);

剩下的就是根据需要使用最近的Pt值。在代码片段中,我从鼠标指针到徒手画线上最近的点画了一条红线。

数学是高效的,并且随着鼠标的移动,这个过程可以实时发生。请参阅代码段。

let isDrawing = false;

  // Set up a stage
  stage = new Konva.Stage({
    container: 'container',
    width: window.innerWidth,
    height: window.innerHeight
  }),

  // add a layer to draw on
  layer = new Konva.Layer(),
    
  mode = 'draw', // state control,  draw = drawing line, measuring = finding nearest point
  lineShape = null,  // the line shape that we draw
  connectorLine = null, // link between mouse and nearest point
  pathShape = null;  // path element


// Add the layer to the stage
stage.add(layer);

// On this event, add a line shape to the canvas - we will extend the points of the line as the mouse moves.   
stage.on('mousedown touchstart', function (e) {
  reset();
  var pos = stage.getPointerPosition();

  if (mode === 'draw'){ // add the line that follows the mouse
    lineShape = new Konva.Line({
      stroke: 'magenta',
      strokeWidth: 5,
      points: [pos.x, pos.y],
      draggable: true
    });
    layer.add(lineShape);
  }
});

// when we finish drawing switch mode to measuring 
stage.on('mouseup touchend', function () {

  // use the pointsToPath fn to prepare a path from the line points.
  thePath = pointsToPath(lineShape.points());
  
  // Make a path shape tracking the lineShape because path has more options for measuring.
  pathShape = new Konva.Path({
    stroke: 'cyan',
    strokeWidth: 5,
    data: thePath            
  });
  layer.add(pathShape);

  lineShape.destroy(); // remove the path shape from the canvas as we are done with it
  
  layer.batchDraw();

  mode='measuring'; // switch the mode
});

// As the mouse is moved we aer concerned first with drawing the line, then measuring the nearest point from the mouse pointer on the line
stage.on('mousemove touchmove', function (e) {

  // get position of mouse pointer 
  const mousePos = stage.getPointerPosition();
  
  if (mode === 'draw' ){
    if (lineShape) { // on first move we will not yet have this shape!
      // drawing the line - extend the line shape by adding the mouse pointer position to the line points array
      const newPoints = lineShape.points().concat([mousePos.x, mousePos.y]);
      lineShape.points(newPoints); // update the line points array
    }
  }
  else {
    // showing nearest point -  link mouse pointer to the closest point on the line
    const closestPt = closestPoint(pathShape, {x: mousePos.x, y: mousePos.y});
    connectorLine.points([closestPt.x, closestPt.y, mousePos.x, mousePos.y]);
  }

  layer.batchDraw();

});


// Function to make a Konva path from the points array of a Konva.Line shape. 
// Returns a path that can be given to a Konva.Path as the .data() value.
// Points array is as [x1, y1, x2, y2, ... xn, yn]
// Path is a string as "M x1, y1 L x2, y2...L xn, yn"
var pointsToPath = function(points){

  let path = '';
  
  for (var i = 0; i < points.length;  i = i + 2){

    switch (i){
        
      case 0:  // move to 
        
        path = path + 'M ' + points[i] + ',' + points[i + 1] + ' ';
        
        break;
        
     default: 
        
        path = path + 'L ' + points[i] + ',' + points[i + 1] + ' ';
        
        break;
  
    }
  }
  return path;
}

// reset the canvas & shapes as needed for a clean restart
function reset() {
  mode = 'draw';
  layer.destroyChildren();
  layer.draw();

  connectorLine = new Konva.Line({
    stroke: 'red',
    strokeWidth: 1,
    points: [0,0, -100, -100]
  })
  layer.add(connectorLine);
}
 
// reset when the user asks
$('#reset').on('click', function(){
  reset();
})
reset(); // reset at startup to prepare state

// From article by https://bl.ocks.org/mbostock at https://bl.ocks.org/mbostock/8027637
// modified as prefixes (VW)
function closestPoint(pathNode, point) {

  var pathLength = pathNode.getLength(), // (VW) replaces pathNode.getTotalLength(),
      precision = 8,
      best,
      bestLength,
      bestDistance = Infinity;

  // linear scan for coarse approximation
  for (var scan, scanLength = 0, scanDistance; scanLength <= pathLength; scanLength += precision) {
    if ((scanDistance = distance2(scan = pathNode.getPointAtLength(scanLength))) < bestDistance) {
      best = scan, bestLength = scanLength, bestDistance = scanDistance;
    }
  }

  // binary search for precise estimate
  precision /= 2;
  while (precision > 0.5) {
    var before,
        after,
        beforeLength,
        afterLength,
        beforeDistance,
        afterDistance;
    if ((beforeLength = bestLength - precision) >= 0 && (beforeDistance = distance2(before = pathNode.getPointAtLength(beforeLength))) < bestDistance) {
      best = before, bestLength = beforeLength, bestDistance = beforeDistance;
    } else if ((afterLength = bestLength + precision) <= pathLength && (afterDistance = distance2(after = pathNode.getPointAtLength(afterLength))) < bestDistance) {
      best = after, bestLength = afterLength, bestDistance = afterDistance;
    } else {
      precision /= 2;
    }
  }

  best = {x: best.x, y: best.y}; // (VW) converted to object instead of array, personal choice
  best.distance = Math.sqrt(bestDistance);
  return best;

  function distance2(p) {
    var dx = p.x - point.x,  // (VW) converter to object from array 
        dy = p.y - point.y;
    return dx * dx + dy * dy;
  }
}
body {
  margin: 10;
  padding: 10;
  overflow: hidden;
  background-color: #f0f0f0;
}
#container {
  width: 600px;
  height: 400px;
  border: 1px solid silver;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva@^3/konva.min.js"></script>
<p>Draw a line by click + drag. Move mouse to show nearest point on line function. </p>
<p>
  <button id = 'reset'>Reset</button></span>
</p>
<div id="container"></div>