问题描述
我需要找到Konva.Line形状上最接近画布上任意点的点。请参见下面的示例,其中鼠标指针是任意点,彩色线是Konva.Line。我特别需要Konvajs实现。
这是一个自我解答的问题,请参阅下面的我的解决方案。我愿意接受任何更好的建议。
解决方法
经过一些网络研究,我发现了一种用于查找路径上最近点的常用算法。参见mbostock的文章。只需很少的更改即可按我的需要进行操作-请参见下面的代码段。
这可以通过使用SVG样式的路径定义,使用get-path-length函数来实现(我坚持使用伪命名,因为您的lib的确切命名可能有所不同,请参见Konva版本的代码段),然后迭代一堆长度上的点的获取(通过获取长度函数获得),以通过简单的数学计算每个点到任意点的距离。因为这样做有处理成本的开销,所以它使用粗步长法来近似,然后使用更精细的二进制方法来快速获得最终结果。结果是一个点-路径上到给定任意点的最近点。
所以-在Konva中启用此功能...注意目标是一条手绘线...
第一个问题是在Konva的上下文中在画布上绘制自由线,您可以使用Line形状。线形具有一系列点,这些点给出沿线的点的坐标。您给它加分,Konva用笔触将这些点连成一条线。通过在每次鼠标移动事件中将线条前进到鼠标指针的位置,即可轻松创建徒手绘制的线条(请参见代码段)。但是,线的点数组没有路径测量功能,因此我们必须将Konva.Line转换为Konva.Path形状,因为该DOES具有所需的路径功能。
将点转换为路径很简单。点数组的布局为[x1,y1,x2,y2,... xn,yn],而路径是一个字符串,布局为“ M x1,y1 L x2,y2 ... L xn,yn” 。它们都可能比这更复杂,但是坚持此简单的连接点线即可满足此要求。该代码段包含pointsToPath()函数。
现在已经找到创建Konva的路径。路径形状很简单。
// 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);
在代码段中,我将线条形状替换为路径形状,但是可能甚至不将形状添加到画布上,而是实例化它以用于最近的点处理。
所以-有了路径,我们可以调用mostestPoint()函数,为它提供鼠标位置和路径形状,以便该函数可以根据需要调用测量和定长获取功能。
// 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]);
剩下的就是根据需要使用最近的值。在代码段中,我从鼠标指针到手绘线的最近点画了一条红线。
数学运算效率很高,并且可以在移动鼠标时实时进行该过程。请参见摘要。
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',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',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,y: mousePos.y});
connectorLine.points([closestPt.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,-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>