在手绘线/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>
我需要找到 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>