围绕一条线创建多边形
Create a polygon around a line
我有一条线表示为 X,Y 坐标数组。我通过 HTML5 canvas 在屏幕上显示此内容并希望提供用户交互。为此,我需要查看用户鼠标是否在线以提供视觉反馈并允许他们移动它等。
该线显示为 "line" 并带有描边,因此仅检查鼠标是否 "on" 该线不会很好地工作,因为这对用户来说很难正好越线。
出于这个原因,我想围绕该线创建一个多边形(本质上是添加填充)。这意味着用户不必直接在线,只需离它很近即可。然后我会使用这个多边形进行命中测试。
如何将点列表(我的线)转换为代表该线的多边形填充? (比如 10px)。
points: [
{ x: -200, y: 150 },
{ x: -100, y: 50 },
{ x: 100, y: 50 },
{ x: 200, y: 150 }
]
I want to create a polygon around the line (essentially adding
padding). This would mean a user didnt have to be directly on the
line, just very close to it. I would then use this polygon for
hit-tests.
你不需要通过数学来实现这个,只需使用内置的 isPointInStroke()
并预先设置 lineWidth
和 lineCap
来增加“灵敏度” (只需使用@Mbo 的 polyfill for isPointInStroke()
for users who uses IE, or take the harder route vie the math as in f.ex. the link provided)。
您可以将路径存储为 Path2D 对象并对其进行命中测试,或者构建当前路径并设置 lineWidth
以进行测试。注意你要测试的路径如果不是当前路径需要重新构建
例子
var ctx = c.getContext("2d"),
points = [
{ x: 10, y: 120 },
{ x: 110, y: 20 },
{ x: 310, y: 20 },
{ x: 410, y: 120 }
];
// create current path and draw polyline
createPath(points);
ctx.stroke();
// increase "padding" and for demo, show area
ctx.lineWidth = 20; // padded area to evaluate
ctx.lineCap = "round"; // caps of line, incl. to evaluate
ctx.strokeStyle = "rgba(200,0,0,0.2)"; // not needed, for demo only
ctx.stroke();
// for sensing mouse
c.onmousemove = function(e) {
var r = this.getBoundingClientRect(),
x = e.clientX - r.left,
y = e.clientY - r.top;
info.innerHTML = ctx.isPointInStroke(x, y) ? "HIT" : "Outside";
};
// build path
function createPath(points) {
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for(var i = 1, p; p = points[i++];) ctx.lineTo(p.x, p.y);
}
<canvas id=c width=600></canvas><br><div id=info></div>
选项#1: 您可以围绕这条线绘制一个多边形,使其成为 "fat target".
选项#2: 您可以使用 isPointInStroke
来测试笔画。
选项#3:纯数学替代。
Math 具有跨浏览器兼容的优势(isPointInStroke
在 IE/Edge 上失败)。
方法如下....
计算鼠标到直线上最近点的距离。
// find XY on line closest to mouse XY
// line shape: {x0:,y0:,x1:,y1:}
// mouse position: mx,my
function closestXY(line,mx,my){
var x0=line.x0;
var y0=line.y0;
var x1=line.x1;
var y1=line.y1;
var dx=x1-x0;
var dy=y1-y0;
var t=((mx-x0)*dx+(my-y0)*dy)/(dx*dx+dy*dy);
t=Math.max(0,Math.min(1,t));
var x=lerp(x0,x1,t);
var y=lerp(y0,y1,t);
return({x:x,y:y});
}
// linear interpolation -- needed in closestXY()
function lerp(a,b,x){return(a+x*(b-a));}
如果该距离在您的 10px "hit range" 范围内,则选择该线。
// is the mouse within 10px of the line
var hitTolerance=10;
var dx=mx-closestPt.x;
var dy=my-closestPt.y;
var distance=Math.sqrt(dx*dx+dy*dy);
if(distance<=hitTolerance){
// this line is w/in 10px of the mouse
}
这是带注释的代码和演示:
// canvas vars
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
function reOffset(){
var BB=canvas.getBoundingClientRect();
offsetX=BB.left;
offsetY=BB.top;
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
window.onresize=function(e){ reOffset(); }
// dragging vars
var isDown=false;
var startX,startY;
// points
var points=[
{ x: 0, y: 150 },
{ x: 100, y: 50 },
{ x: 300, y: 50 },
{ x: 500, y: 150 }
]
// create lines from points
var lines=[];
for(var i=1;i<points.length;i++){
lines.push({
x0:points[i-1].x,
y0:points[i-1].y,
x1:points[i].x,
y1:points[i].y,
});
}
// if the mouse is within 10px of a line, it's selected
var hitTolerance=10;
// just an efficiency to avoid the expensive Math.sqrt
var hitToleranceSquared=hitTolerance*hitTolerance;
// on mousedown, "nearest" holds any line w/in 10px of the mouse
var nearest=null;
// draw the scene for the first time
draw();
// listen for mouse events
$("#canvas").mousedown(function(e){handleMouseDown(e);});
$("#canvas").mousemove(function(e){handleMouseMove(e);});
$("#canvas").mouseup(function(e){handleMouseUpOut(e);});
$("#canvas").mouseout(function(e){handleMouseUpOut(e);});
// functions
//////////////////////////
// select the nearest line to the mouse
function closestLine(mx,my){
var dist=100000000;
var index,pt;
// test the mouse vs each line -- find the closest line
for(var i=0;i<lines.length;i++){
// find the XY point on the line that's closest to mouse
var xy=closestXY(lines[i],mx,my);
//
var dx=mx-xy.x;
var dy=my-xy.y;
var thisDist=dx*dx+dy*dy;
if(thisDist<dist){
dist=thisDist;
pt=xy;
index=i;
}
}
// test if the closest line is within the hit distance
if(dist<=hitToleranceSquared){
var line=lines[index];
return({ pt:pt, line:line, originalLine:{x0:line.x0,y0:line.y0,x1:line.x1,y1:line.y1} });
}else{
return(null);
}
}
// linear interpolation -- needed in setClosestLine()
function lerp(a,b,x){return(a+x*(b-a));}
// find closest XY on line to mouse XY
function closestXY(line,mx,my){
var x0=line.x0;
var y0=line.y0;
var x1=line.x1;
var y1=line.y1;
var dx=x1-x0;
var dy=y1-y0;
var t=((mx-x0)*dx+(my-y0)*dy)/(dx*dx+dy*dy);
t=Math.max(0,Math.min(1,t));
var x=lerp(x0,x1,t);
var y=lerp(y0,y1,t);
return({x:x,y:y});
}
// draw the scene
function draw(){
ctx.clearRect(0,0,cw,ch);
// draw all lines at their current positions
for(var i=0;i<lines.length;i++){
drawLine(lines[i],'black');
}
// draw markers if a line is being dragged
if(nearest){
// point on line nearest to mouse
ctx.beginPath();
ctx.arc(nearest.pt.x,nearest.pt.y,5,0,Math.PI*2);
ctx.strokeStyle='red';
ctx.stroke();
// marker for original line before dragging
drawLine(nearest.originalLine,'red');
// hightlight the line as its dragged
drawLine(nearest.line,'red');
}
}
function drawLine(line,color){
ctx.beginPath();
ctx.moveTo(line.x0,line.y0);
ctx.lineTo(line.x1,line.y1);
ctx.strokeStyle=color;
ctx.stroke();
}
function handleMouseDown(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// mouse position
startX=parseInt(e.clientX-offsetX);
startY=parseInt(e.clientY-offsetY);
// find nearest line to mouse
nearest=closestLine(startX,startY);
// set dragging flag if a line was w/in hit distance
if(nearest){
isDown=true;
draw();
}
}
function handleMouseUpOut(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// clear dragging flag
isDown=false;
nearest=null;
draw();
}
function handleMouseMove(e){
if(!isDown){return;}
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// mouse position
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
// calc how far mouse has moved since last mousemove event
var dx=mouseX-startX;
var dy=mouseY-startY;
startX=mouseX;
startY=mouseY;
// change nearest line vertices by distance moved
var line=nearest.line;
line.x0+=dx;
line.y0+=dy;
line.x1+=dx;
line.y1+=dy;
// redraw
draw();
}
body{ background-color: ivory; }
#canvas{border:1px solid red; margin:0 auto; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Drag lines with mouse.<br>You must start drag within 10px of line</h4>
<canvas id="canvas" width=550 height=300></canvas>
附近的线、线段和点。
对于所涉及的数学,有两个函数可以提供帮助,
点到直线的距离。
以下函数计算点到直线的距离。
// return distance of point (px,py) to line ((l1x,l1y),(l2x,l2y))
distPoint2Line = function(px, py, l1x, l1y, l2x, l2y){
var v1x,v1y,v2x,v2y,l,c;
v1x = l2x - l1x; // convert the line to a vector basicly moves the line
v1y = l2y - l1y; // so that it starts at 0,0
v2x = px - l1x; // shift the point the same distance so it is in the
v2y = py - l1y; // same relative position
// Useful math trick
// The following finds the unit length of the closest point
// on the line vector V1 to the point v2
// u is unbounded and can have any value but if it is
// 0 <= u <= 1 then that point is on the line where
// where u = 0 is the start u = 0.5 the middle and u = 1 the end
// u < 0 is before the line start and u > 1 is after the line end
// in math gargon. Get the dot product of V2 . V1 divided by the length squared of V1
u = (v2x * v1x + v2y * v1y)/(v1x * v1x + v1y * v1y);
// Now if we multiply the vector of the line V1 by c we get the
// coordinates of the closest point on the line
v1x *= u;
v1y *= u;
// now it is simple to find the distance from that point on the
// line to the point via pythagoras
v1x -= v2x; // distance between the two points
v1y -= v2y;
// sqrt of the sum of the square of the sides
return Math.sqrt(v1x * v1x + v1y * v1y);
}
点到线段的距离
你现在有一个点到直线的距离,但问题是直线是无限长的。我们想要找到具有明确起点、终点和长度的线段的距离。
如果您阅读上面代码中的注释,您会发现我们已经拥有该函数中所需的一切。特别是单位距离 u 如果我们限制该值(保持它以便 0 <= u <= 1),上面的函数将为我们提供距线段的距离,如果点移动超过起点或终点,则距离将从起点或终点,以最接近的为准。
// return distance of point (px,py) to line ((l1x,l1y),(l2x,l2y))
distPoint2Line = function(px, py, l1x, l1y, l2x, l2y){
var v1x,v1y,v2x,v2y,l,c;
v1x = l2x - l1x; // convert the line to a vector basicly moves the line
v1y = l2y - l1y; // so that it starts at 0,0
v2x = px - l1x; // shift the point the same distance so it is in the
v2y = py - l1y; // same relative position
// get unit distance
u = (v2x * v1x + v2y * v1y)/(v1x * v1x + v2x * v2x);
// clamp it
u = Math.max(0,Math.Min(1,u)); // if below 0 make it 0 if above 1 make it 1
v1x *= u; // multiply the line vector
v1y *= u;
v1x -= v2x; // distance between the two points x and y components
v1y -= v2y;
// sqrt of the sum of the square of the sides gives the distance
return Math.sqrt(v1x,v1y);
}
什么时候一行不是一行?
在某些情况下,由两个点(有效数字)描述的线不是我们可以处理的线。长度为零的线(终点和起点都在同一坐标)和一个或两个点都在无穷远的无限线段
有起点和终点的点
当一行传递给结束和开始在同一点的函数时怎么办。当这种情况发生时,线段长度为 0,因为我们除以 0 的平方(0*0 仍然是 0)Javascript returns Infinity 并且从那里事情开始变得混乱并且 return 值为 NaN
(不是数字)。那么该怎么办。我们不能将它保留为 NaN
,因为那是 Javascript 中的思想 f..k 并且没有任何东西等于 NaN
(甚至 NaN == NaN
)所以你将不得不调用另一个功能。太啰嗦了,不适合这个问题。
另一种处理它的方法是 return undefined
或 null
但这同样是一个糟糕的解决方案,因为这意味着无论何时使用该功能,您都必须审查其结果。
如果考虑单位距离u
。 Infinity
是正确答案,但我们不知道直线行进的方向,但我们确实知道二维 space 中的一个点,直线与 return 的距离那一点确实符合约束条件,并允许一个有意义的结果,可以相信它是一个数字。
所以代码mod有点
u = (v1x * v1x + v2x * v2x);
u = u === 0 ? 0 : (v2x * v1x + v2y * v1y) / u; // if u is 0 then set it as 0
这将继续给出一个结果,该结果是与线 ((l1x,l1y),(l2x,l2y)) 描述的无限小且无方向的线段的距离,并且在问题的上下文中有有价值且正确的含义。
线段也是如此
一条无限长的线段
可能会出现某些计算的结果将起点和终点的一个或两个坐标设置在Infinity
或-Infinity
,也可能只是一个坐标x,或y。发生这种情况时,我们会立即得到 NaN
。
我们可以通过检查所有进入函数的点来处理它。但这对于绝大多数情况来说是不需要的开销。我会忽略这种情况,这只是为了让人们知道在某些情况下这是可能的,如果您觉得需要安全,就应该进行审查。
现在我们不必检查该函数的每个结果,并且可以相信它具有适用于查找与直线的距离的意义。
兼容性问题
最后一件事是浏览器兼容性。在新的(好岁的)ES6(ECMAScript 6)中有数学函数 Math.hypot
即 return 是一组坐标 2D,3D,...,nD 的斜边 它是这样的有用的功能,并且比 Math.sqrt(Math.pow(x,2) + Math.pow(y,2))
快得多,我个人决定不忽略它。因此,我提供了一个 polyfill,它涵盖了这个问题的 2D 需求,为了更好的解决方案,我让你在网上找到一个。如果这不是您的政策,则将所有 Math.hypot(x,y)
替换为 Math.sqrt(Math.pow(x,2) + Math.pow(y,2))
总结一下
所以现在把它全部清理成一个有用的包
这个有
- lineHelper.distPoint2Line(px, py, l1x, l1y, l2x, l2y) returns点(px,py)到直线的距离((l1x, l1y), (l2x, l2y))
- lineHelper.distPoint2LineSeg(px, py, l1x, l1y, l2x, l2y) 与上面相同,但线段而不是线
- lineHelper.indexOfLineClosest2Point(px, py, array, closed) 找到距离数组一所在点(px, py)最近的线段
维数组作为描述线 [p1x, p1y,
p2x, p2y, ..., pnx, pny]。 closed 是可选的,如果不是 false 将
考虑数组中的终点和起点是一条线段
好从而关闭路径。索引是从0开始的绝对索引
对于第一个点,第二个点为 2,最后一个点为 (n-1)*2
观点。 returned 值将始终为偶数或 0。
- lineHelper.getPointOnLine() 作为上述计算的结果在线上的点被存储并且可以被检索
有了这个电话。它将 return 一个由两个数字组成的数组,代表
线段上的坐标,即 [x,y]。对于线路,这一点可能
在线段之外。对于线,这一点将在线上
或者在起点或终点。如果在终点,它可能会在
此类数学中固有的浮点错误。采用
Math.EPSILON
检查这是在末尾还是在后面
查看单位距离是否为 1 的函数
- lineHelper.getUnitDist() 作为计算的结果 returns 沿直线的单位距离是距离直线最近的点
计算中给出的点。它被钳制为线段和
松开线。如果 line/line 段是一个点,它将
为 0。这对
indexOfLineClosest2Point
无效,可能
可以是任何值。
- lineHelper.getMinDist() 作为函数的结果
indexOfLineClosest2Point
这个函数将 return 到那个找到的线段的距离功能。此值仅在调用 indexOfLineClosest2Point
之后有效,直到再次调用该函数。
和代码
var lineHelper = (function(){ // call it what you want
var hypot = Math.hypot;
if(typeof hypot !== "function"){ // poly fill for hypot
hypot = function(x,y){
return Math.sqrt(x * x + y * y);
}
}
var lenSq, unitDist, minDist, v1x, v1y, v2x, v2y, lsx, lsy, vx,vy; // closure vars
var dP2L = function(px, py, l1x, l1y, l2x, l2y){
v1x = l2x - l1x;
v1y = l2y - l1y;
v2x = px - (lsx = l1x);
v2y = py - (lsy = l1y);
unitDist = (v1x * v1x + v1y * v1y);
unitDist = unitDist === 0 ? 0 : (v2x * v1x + v2y * v1y) / unitDist;
return hypot((v1x *= unitDist) - v2x, (v1y *= unitDist) - v2y);
}
var dP2LS = function(px, py, l1x, l1y, l2x, l2y){
v1x = l2x - l1x;
v1y = l2y - l1y;
v2x = px - (lsx = l1x);
v2y = py - (lsy = l1y);
unitDist = (v1x * v1x + v1y * v1y);
unitDist = unitDist === 0 ? 0 : Math.max(0, Math.min(1, (v2x * v1x + v2y * v1y) / unitDist));
return hypot((v1x *= unitDist) - v2x, (v1y *= unitDist) - v2y);
}
var dP2V = function(px, py, l1x, l1y){ // point dist to vector
unitDist = (l1x * l1x + l1y * l1y);
unitDist = unitDist === 0 ? 0 : unitDist = Math.max(0, Math.min(1, (px * l1x + py * l1y) / unitDist));
return hypot((v1x = l1x * unitDist) - px, (v1y = l1y * unitDist) - py);
}
var cLineSeg = function(px, py, array, closed){
var i, len, leni, dist, lineIndex;
minDist = Infinity;
leni = len = array.length;
if(! closed){
leni -= 2;
}
for(i = 0; i < leni; i += 2){
dist = dP2V(px - array[i], py - array[i + 1], array[(i + 2) % len] - array[i], array[(i + 3) % len] - array[i +1]);
if(dist < minDist){
lineIndex = i;
minDist = dist;
lsx = array[i];
lsy = array[i + 1];
vx = v1x;
vy = v1y;
}
}
v1x = vx;
v1y = vy;
return lineIndex;
}
return {
distPoint2Line : dP2L,
distPoint2LineSeg : dP2LS,
indexOfLineClosest2Point : cLineSeg,
getPointOnLine : function(){ return [lsx + v1x,lsy + v1y] },
getUnitDist : function() { return unitDist; },
getMinDist : function() { return minDist; },
}
})();
我有一条线表示为 X,Y 坐标数组。我通过 HTML5 canvas 在屏幕上显示此内容并希望提供用户交互。为此,我需要查看用户鼠标是否在线以提供视觉反馈并允许他们移动它等。
该线显示为 "line" 并带有描边,因此仅检查鼠标是否 "on" 该线不会很好地工作,因为这对用户来说很难正好越线。
出于这个原因,我想围绕该线创建一个多边形(本质上是添加填充)。这意味着用户不必直接在线,只需离它很近即可。然后我会使用这个多边形进行命中测试。
如何将点列表(我的线)转换为代表该线的多边形填充? (比如 10px)。
points: [
{ x: -200, y: 150 },
{ x: -100, y: 50 },
{ x: 100, y: 50 },
{ x: 200, y: 150 }
]
I want to create a polygon around the line (essentially adding padding). This would mean a user didnt have to be directly on the line, just very close to it. I would then use this polygon for hit-tests.
你不需要通过数学来实现这个,只需使用内置的 isPointInStroke()
并预先设置 lineWidth
和 lineCap
来增加“灵敏度” (只需使用@Mbo 的 polyfill for isPointInStroke()
for users who uses IE, or take the harder route vie the math as in f.ex. the link provided)。
您可以将路径存储为 Path2D 对象并对其进行命中测试,或者构建当前路径并设置 lineWidth
以进行测试。注意你要测试的路径如果不是当前路径需要重新构建
例子
var ctx = c.getContext("2d"),
points = [
{ x: 10, y: 120 },
{ x: 110, y: 20 },
{ x: 310, y: 20 },
{ x: 410, y: 120 }
];
// create current path and draw polyline
createPath(points);
ctx.stroke();
// increase "padding" and for demo, show area
ctx.lineWidth = 20; // padded area to evaluate
ctx.lineCap = "round"; // caps of line, incl. to evaluate
ctx.strokeStyle = "rgba(200,0,0,0.2)"; // not needed, for demo only
ctx.stroke();
// for sensing mouse
c.onmousemove = function(e) {
var r = this.getBoundingClientRect(),
x = e.clientX - r.left,
y = e.clientY - r.top;
info.innerHTML = ctx.isPointInStroke(x, y) ? "HIT" : "Outside";
};
// build path
function createPath(points) {
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for(var i = 1, p; p = points[i++];) ctx.lineTo(p.x, p.y);
}
<canvas id=c width=600></canvas><br><div id=info></div>
选项#1: 您可以围绕这条线绘制一个多边形,使其成为 "fat target".
选项#2: 您可以使用 isPointInStroke
来测试笔画。
选项#3:纯数学替代。
Math 具有跨浏览器兼容的优势(isPointInStroke
在 IE/Edge 上失败)。
方法如下....
计算鼠标到直线上最近点的距离。
// find XY on line closest to mouse XY
// line shape: {x0:,y0:,x1:,y1:}
// mouse position: mx,my
function closestXY(line,mx,my){
var x0=line.x0;
var y0=line.y0;
var x1=line.x1;
var y1=line.y1;
var dx=x1-x0;
var dy=y1-y0;
var t=((mx-x0)*dx+(my-y0)*dy)/(dx*dx+dy*dy);
t=Math.max(0,Math.min(1,t));
var x=lerp(x0,x1,t);
var y=lerp(y0,y1,t);
return({x:x,y:y});
}
// linear interpolation -- needed in closestXY()
function lerp(a,b,x){return(a+x*(b-a));}
如果该距离在您的 10px "hit range" 范围内,则选择该线。
// is the mouse within 10px of the line
var hitTolerance=10;
var dx=mx-closestPt.x;
var dy=my-closestPt.y;
var distance=Math.sqrt(dx*dx+dy*dy);
if(distance<=hitTolerance){
// this line is w/in 10px of the mouse
}
这是带注释的代码和演示:
// canvas vars
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
function reOffset(){
var BB=canvas.getBoundingClientRect();
offsetX=BB.left;
offsetY=BB.top;
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
window.onresize=function(e){ reOffset(); }
// dragging vars
var isDown=false;
var startX,startY;
// points
var points=[
{ x: 0, y: 150 },
{ x: 100, y: 50 },
{ x: 300, y: 50 },
{ x: 500, y: 150 }
]
// create lines from points
var lines=[];
for(var i=1;i<points.length;i++){
lines.push({
x0:points[i-1].x,
y0:points[i-1].y,
x1:points[i].x,
y1:points[i].y,
});
}
// if the mouse is within 10px of a line, it's selected
var hitTolerance=10;
// just an efficiency to avoid the expensive Math.sqrt
var hitToleranceSquared=hitTolerance*hitTolerance;
// on mousedown, "nearest" holds any line w/in 10px of the mouse
var nearest=null;
// draw the scene for the first time
draw();
// listen for mouse events
$("#canvas").mousedown(function(e){handleMouseDown(e);});
$("#canvas").mousemove(function(e){handleMouseMove(e);});
$("#canvas").mouseup(function(e){handleMouseUpOut(e);});
$("#canvas").mouseout(function(e){handleMouseUpOut(e);});
// functions
//////////////////////////
// select the nearest line to the mouse
function closestLine(mx,my){
var dist=100000000;
var index,pt;
// test the mouse vs each line -- find the closest line
for(var i=0;i<lines.length;i++){
// find the XY point on the line that's closest to mouse
var xy=closestXY(lines[i],mx,my);
//
var dx=mx-xy.x;
var dy=my-xy.y;
var thisDist=dx*dx+dy*dy;
if(thisDist<dist){
dist=thisDist;
pt=xy;
index=i;
}
}
// test if the closest line is within the hit distance
if(dist<=hitToleranceSquared){
var line=lines[index];
return({ pt:pt, line:line, originalLine:{x0:line.x0,y0:line.y0,x1:line.x1,y1:line.y1} });
}else{
return(null);
}
}
// linear interpolation -- needed in setClosestLine()
function lerp(a,b,x){return(a+x*(b-a));}
// find closest XY on line to mouse XY
function closestXY(line,mx,my){
var x0=line.x0;
var y0=line.y0;
var x1=line.x1;
var y1=line.y1;
var dx=x1-x0;
var dy=y1-y0;
var t=((mx-x0)*dx+(my-y0)*dy)/(dx*dx+dy*dy);
t=Math.max(0,Math.min(1,t));
var x=lerp(x0,x1,t);
var y=lerp(y0,y1,t);
return({x:x,y:y});
}
// draw the scene
function draw(){
ctx.clearRect(0,0,cw,ch);
// draw all lines at their current positions
for(var i=0;i<lines.length;i++){
drawLine(lines[i],'black');
}
// draw markers if a line is being dragged
if(nearest){
// point on line nearest to mouse
ctx.beginPath();
ctx.arc(nearest.pt.x,nearest.pt.y,5,0,Math.PI*2);
ctx.strokeStyle='red';
ctx.stroke();
// marker for original line before dragging
drawLine(nearest.originalLine,'red');
// hightlight the line as its dragged
drawLine(nearest.line,'red');
}
}
function drawLine(line,color){
ctx.beginPath();
ctx.moveTo(line.x0,line.y0);
ctx.lineTo(line.x1,line.y1);
ctx.strokeStyle=color;
ctx.stroke();
}
function handleMouseDown(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// mouse position
startX=parseInt(e.clientX-offsetX);
startY=parseInt(e.clientY-offsetY);
// find nearest line to mouse
nearest=closestLine(startX,startY);
// set dragging flag if a line was w/in hit distance
if(nearest){
isDown=true;
draw();
}
}
function handleMouseUpOut(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// clear dragging flag
isDown=false;
nearest=null;
draw();
}
function handleMouseMove(e){
if(!isDown){return;}
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// mouse position
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
// calc how far mouse has moved since last mousemove event
var dx=mouseX-startX;
var dy=mouseY-startY;
startX=mouseX;
startY=mouseY;
// change nearest line vertices by distance moved
var line=nearest.line;
line.x0+=dx;
line.y0+=dy;
line.x1+=dx;
line.y1+=dy;
// redraw
draw();
}
body{ background-color: ivory; }
#canvas{border:1px solid red; margin:0 auto; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Drag lines with mouse.<br>You must start drag within 10px of line</h4>
<canvas id="canvas" width=550 height=300></canvas>
附近的线、线段和点。
对于所涉及的数学,有两个函数可以提供帮助,
点到直线的距离。
以下函数计算点到直线的距离。
// return distance of point (px,py) to line ((l1x,l1y),(l2x,l2y))
distPoint2Line = function(px, py, l1x, l1y, l2x, l2y){
var v1x,v1y,v2x,v2y,l,c;
v1x = l2x - l1x; // convert the line to a vector basicly moves the line
v1y = l2y - l1y; // so that it starts at 0,0
v2x = px - l1x; // shift the point the same distance so it is in the
v2y = py - l1y; // same relative position
// Useful math trick
// The following finds the unit length of the closest point
// on the line vector V1 to the point v2
// u is unbounded and can have any value but if it is
// 0 <= u <= 1 then that point is on the line where
// where u = 0 is the start u = 0.5 the middle and u = 1 the end
// u < 0 is before the line start and u > 1 is after the line end
// in math gargon. Get the dot product of V2 . V1 divided by the length squared of V1
u = (v2x * v1x + v2y * v1y)/(v1x * v1x + v1y * v1y);
// Now if we multiply the vector of the line V1 by c we get the
// coordinates of the closest point on the line
v1x *= u;
v1y *= u;
// now it is simple to find the distance from that point on the
// line to the point via pythagoras
v1x -= v2x; // distance between the two points
v1y -= v2y;
// sqrt of the sum of the square of the sides
return Math.sqrt(v1x * v1x + v1y * v1y);
}
点到线段的距离
你现在有一个点到直线的距离,但问题是直线是无限长的。我们想要找到具有明确起点、终点和长度的线段的距离。
如果您阅读上面代码中的注释,您会发现我们已经拥有该函数中所需的一切。特别是单位距离 u 如果我们限制该值(保持它以便 0 <= u <= 1),上面的函数将为我们提供距线段的距离,如果点移动超过起点或终点,则距离将从起点或终点,以最接近的为准。
// return distance of point (px,py) to line ((l1x,l1y),(l2x,l2y))
distPoint2Line = function(px, py, l1x, l1y, l2x, l2y){
var v1x,v1y,v2x,v2y,l,c;
v1x = l2x - l1x; // convert the line to a vector basicly moves the line
v1y = l2y - l1y; // so that it starts at 0,0
v2x = px - l1x; // shift the point the same distance so it is in the
v2y = py - l1y; // same relative position
// get unit distance
u = (v2x * v1x + v2y * v1y)/(v1x * v1x + v2x * v2x);
// clamp it
u = Math.max(0,Math.Min(1,u)); // if below 0 make it 0 if above 1 make it 1
v1x *= u; // multiply the line vector
v1y *= u;
v1x -= v2x; // distance between the two points x and y components
v1y -= v2y;
// sqrt of the sum of the square of the sides gives the distance
return Math.sqrt(v1x,v1y);
}
什么时候一行不是一行?
在某些情况下,由两个点(有效数字)描述的线不是我们可以处理的线。长度为零的线(终点和起点都在同一坐标)和一个或两个点都在无穷远的无限线段
有起点和终点的点
当一行传递给结束和开始在同一点的函数时怎么办。当这种情况发生时,线段长度为 0,因为我们除以 0 的平方(0*0 仍然是 0)Javascript returns Infinity 并且从那里事情开始变得混乱并且 return 值为 NaN
(不是数字)。那么该怎么办。我们不能将它保留为 NaN
,因为那是 Javascript 中的思想 f..k 并且没有任何东西等于 NaN
(甚至 NaN == NaN
)所以你将不得不调用另一个功能。太啰嗦了,不适合这个问题。
另一种处理它的方法是 return undefined
或 null
但这同样是一个糟糕的解决方案,因为这意味着无论何时使用该功能,您都必须审查其结果。
如果考虑单位距离u
。 Infinity
是正确答案,但我们不知道直线行进的方向,但我们确实知道二维 space 中的一个点,直线与 return 的距离那一点确实符合约束条件,并允许一个有意义的结果,可以相信它是一个数字。
所以代码mod有点
u = (v1x * v1x + v2x * v2x);
u = u === 0 ? 0 : (v2x * v1x + v2y * v1y) / u; // if u is 0 then set it as 0
这将继续给出一个结果,该结果是与线 ((l1x,l1y),(l2x,l2y)) 描述的无限小且无方向的线段的距离,并且在问题的上下文中有有价值且正确的含义。
线段也是如此
一条无限长的线段
可能会出现某些计算的结果将起点和终点的一个或两个坐标设置在Infinity
或-Infinity
,也可能只是一个坐标x,或y。发生这种情况时,我们会立即得到 NaN
。
我们可以通过检查所有进入函数的点来处理它。但这对于绝大多数情况来说是不需要的开销。我会忽略这种情况,这只是为了让人们知道在某些情况下这是可能的,如果您觉得需要安全,就应该进行审查。
现在我们不必检查该函数的每个结果,并且可以相信它具有适用于查找与直线的距离的意义。
兼容性问题
最后一件事是浏览器兼容性。在新的(好岁的)ES6(ECMAScript 6)中有数学函数 Math.hypot
即 return 是一组坐标 2D,3D,...,nD 的斜边 它是这样的有用的功能,并且比 Math.sqrt(Math.pow(x,2) + Math.pow(y,2))
快得多,我个人决定不忽略它。因此,我提供了一个 polyfill,它涵盖了这个问题的 2D 需求,为了更好的解决方案,我让你在网上找到一个。如果这不是您的政策,则将所有 Math.hypot(x,y)
替换为 Math.sqrt(Math.pow(x,2) + Math.pow(y,2))
总结一下
所以现在把它全部清理成一个有用的包
这个有
- lineHelper.distPoint2Line(px, py, l1x, l1y, l2x, l2y) returns点(px,py)到直线的距离((l1x, l1y), (l2x, l2y))
- lineHelper.distPoint2LineSeg(px, py, l1x, l1y, l2x, l2y) 与上面相同,但线段而不是线
- lineHelper.indexOfLineClosest2Point(px, py, array, closed) 找到距离数组一所在点(px, py)最近的线段 维数组作为描述线 [p1x, p1y, p2x, p2y, ..., pnx, pny]。 closed 是可选的,如果不是 false 将 考虑数组中的终点和起点是一条线段 好从而关闭路径。索引是从0开始的绝对索引 对于第一个点,第二个点为 2,最后一个点为 (n-1)*2 观点。 returned 值将始终为偶数或 0。
- lineHelper.getPointOnLine() 作为上述计算的结果在线上的点被存储并且可以被检索
有了这个电话。它将 return 一个由两个数字组成的数组,代表
线段上的坐标,即 [x,y]。对于线路,这一点可能
在线段之外。对于线,这一点将在线上
或者在起点或终点。如果在终点,它可能会在
此类数学中固有的浮点错误。采用
Math.EPSILON
检查这是在末尾还是在后面 查看单位距离是否为 1 的函数
- lineHelper.getUnitDist() 作为计算的结果 returns 沿直线的单位距离是距离直线最近的点
计算中给出的点。它被钳制为线段和
松开线。如果 line/line 段是一个点,它将
为 0。这对
indexOfLineClosest2Point
无效,可能 可以是任何值。 - lineHelper.getMinDist() 作为函数的结果
indexOfLineClosest2Point
这个函数将 return 到那个找到的线段的距离功能。此值仅在调用indexOfLineClosest2Point
之后有效,直到再次调用该函数。
和代码
var lineHelper = (function(){ // call it what you want
var hypot = Math.hypot;
if(typeof hypot !== "function"){ // poly fill for hypot
hypot = function(x,y){
return Math.sqrt(x * x + y * y);
}
}
var lenSq, unitDist, minDist, v1x, v1y, v2x, v2y, lsx, lsy, vx,vy; // closure vars
var dP2L = function(px, py, l1x, l1y, l2x, l2y){
v1x = l2x - l1x;
v1y = l2y - l1y;
v2x = px - (lsx = l1x);
v2y = py - (lsy = l1y);
unitDist = (v1x * v1x + v1y * v1y);
unitDist = unitDist === 0 ? 0 : (v2x * v1x + v2y * v1y) / unitDist;
return hypot((v1x *= unitDist) - v2x, (v1y *= unitDist) - v2y);
}
var dP2LS = function(px, py, l1x, l1y, l2x, l2y){
v1x = l2x - l1x;
v1y = l2y - l1y;
v2x = px - (lsx = l1x);
v2y = py - (lsy = l1y);
unitDist = (v1x * v1x + v1y * v1y);
unitDist = unitDist === 0 ? 0 : Math.max(0, Math.min(1, (v2x * v1x + v2y * v1y) / unitDist));
return hypot((v1x *= unitDist) - v2x, (v1y *= unitDist) - v2y);
}
var dP2V = function(px, py, l1x, l1y){ // point dist to vector
unitDist = (l1x * l1x + l1y * l1y);
unitDist = unitDist === 0 ? 0 : unitDist = Math.max(0, Math.min(1, (px * l1x + py * l1y) / unitDist));
return hypot((v1x = l1x * unitDist) - px, (v1y = l1y * unitDist) - py);
}
var cLineSeg = function(px, py, array, closed){
var i, len, leni, dist, lineIndex;
minDist = Infinity;
leni = len = array.length;
if(! closed){
leni -= 2;
}
for(i = 0; i < leni; i += 2){
dist = dP2V(px - array[i], py - array[i + 1], array[(i + 2) % len] - array[i], array[(i + 3) % len] - array[i +1]);
if(dist < minDist){
lineIndex = i;
minDist = dist;
lsx = array[i];
lsy = array[i + 1];
vx = v1x;
vy = v1y;
}
}
v1x = vx;
v1y = vy;
return lineIndex;
}
return {
distPoint2Line : dP2L,
distPoint2LineSeg : dP2LS,
indexOfLineClosest2Point : cLineSeg,
getPointOnLine : function(){ return [lsx + v1x,lsy + v1y] },
getUnitDist : function() { return unitDist; },
getMinDist : function() { return minDist; },
}
})();