konvajs - 在手绘线 / Konva.Line 形状上找到最近的 x 和 y 点,到画布上的一个点
问题描述
我需要在 Konva.Line 形状上找到离画布上任意点最近的点。请参阅下面的示例,其中鼠标指针是任意点,彩色线是 Konva.Line。我特别需要一个 Konvajs 实现。
这是一个自我回答的问题,请参阅下面的解决方案。我愿意接受任何更好的建议。
解决方案
经过一些网络研究,我发现了一种常用的算法来查找路径上的最近点。请参阅mbostock的文章。这只需很少的更改即可按我的需要进行操作 - 请参见下面的代码片段。
这通过采用 SVG 样式的路径定义,使用 get-path-length 函数来工作(我在这里坚持伪命名,因为您的库可能在确切命名上有所不同,请参阅 Konva 版本的片段)然后遍历一堆点由 get-point-at-length 函数找到的路径,通过简单的数学计算从每个点到任意点的距离。因为这会产生处理成本开销,所以它使用粗略的步长来获得近似值,然后使用更精细的二进制方法来快速获得最终结果。结果是一个点 - 到给定任意点的路径上最近的点。
所以 - 在 Konva 中启用它......注意目标是一条自由手绘线......
第一个问题是,要在 Konva 的上下文中在画布上绘制一条徒手线,您可以使用 Line 形状。Line 形状有一个点数组,这些点给出了沿线的点的坐标。你给它点数,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);
在片段中,我用路径形状替换了线条形状,但甚至可以不将形状添加到画布上,而是将其实例化以用于最近点处理。
所以 - 有了路径,我们可以调用最接近点()函数,给它鼠标位置和路径形状,以便函数可以根据需要调用测量和长度获取函数。
// 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',
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>
推荐阅读
- c++ - 在 Jenkins 中运行 make 命令
- javascript - React Native 函数绑定参数
- jquery - 如何在 React 中使用“draggable()”函数?
- kotlin - 如何按值对对象进行分组
- javascript - React - 单击按钮后显示组件
- fancybox - 不要关闭fancybox然后重新加载父站点
- r - R:函数的形式在哪里存储在内存中?
- java - java.lang.ClassNotFoundException: org.apache.derby.jdbc.EmbeddedDriver(包括 JAR)
- pattern-matching - 匹配 ocaml 中的复杂数据类型
- raku - 在 Perl 6 中,如何使用 NativeCall 接口将原始字节转换为浮点?