在一条线

时间:2016-04-21 15:25:12

标签: javascript math canvas html5-canvas drawing

我有一条线表示为X,Y坐标的数组。我通过HTML5画布在屏幕上显示此内容,并希望提供用户交互。为此,我需要查看用户鼠标是否在线以提供视觉反馈并允许他们移动它等。

该行显示为" line"中风给它一个厚度,所以只需检查鼠标是否在" on"这条线路不会很好用,因为用户很难完全超越线路。

出于这个原因,我想在线周围创建一个多边形(实际上是添加填充)。这意味着用户不必直接在线,只是非常接近它。然后我会使用这个多边形进行命中测试。

如何将点列表(我的线)转换为表示带填充线的多边形? (比如10px)。

points: [
    { x: -200, y: 150 },
    { x: -100, y: 50 },
    { x: 100, y: 50 },
    { x: 200, y: 150 }
]

3 个答案:

答案 0 :(得分:2)

  

我想在线周围创建一个多边形(基本上是添加   填充)。这意味着用户不必直接在   线,非常接近它。然后我会使用这个多边形   命中测试。

您无需通过数学计算来实现此目的,只需使用内置isPointInStroke()并预先设置lineWidthlineCap以增加“灵敏度”(仅对于使用IE的用户,请使用此polyfill作为isPointInStroke(),或者使用更难的路径,如f.ex.中的link provided通过@Mbo进行数学运算。

您可以将路径存储为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 :(得分:2)

选项#1:您可以在线周围绘制一个多边形,使其成为&#34;胖目标&#34;。

选项#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&#34;命中范围内&#34;然后选择该行。

// 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
}

以下是带注释的代码和演示:

&#13;
&#13;
// 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();
}
&#13;
body{ background-color: ivory; }
#canvas{border:1px solid red; margin:0 auto; }
&#13;
<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>
&#13;
&#13;
&#13;

答案 2 :(得分: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
    // 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);
}

什么时候一行不是一行?

在某些情况下,由两点(有效数字)描述的线不是我们可以处理的线。长度为零的线(端点和起点都在同一坐标处)和无限线段,其中一个或两个点位于无穷大

包含开头和结尾的点

当一行传递给end和start位于同一点的函数时该怎么办。当发生这种情况时,线段长度为0,因为我们除以0的平方(0 * 0仍为0)Javascript返回无穷大并且从那里开始变得混乱并且返回值是NaN(不是数)。那么该怎么办。我们不能将其保留为NaN,因为这是Javascript中的一个大脑f..k并且没有任何东西等于NaN(甚至不是NaN == NaN)所以你必须调用另一个函数。太长时间不知所措,不适合这个问题。

另一种处理它的方法是返回undefinednull,但这又是一个糟糕的解决方案,因为这意味着无论何时使用该功能,您都必须审查其结果。

如果考虑单位距离uInfinity是正确的答案,但我们不知道线路的行进方向,但我们知道2D空间中的一个点,其中线路返回到该点的距离确实符合约束条件允许有意义的结果,可以信任为数字。

因此代码中有一点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,它返回一组坐标2D,3D,...,nD的斜边这是一个非常有用的函数,是这样的比Math.sqrt(Math.pow(x,2) + Math.pow(y,2))快得多,我个人决定不去理它。因此,我提供了一个覆盖这个问题的2D需求的polyfill,为了更好的解决方案,我让你在网上找到一个。如果这不是您的政策,请将所有Math.hypot(x,y)替换为Math.sqrt(Math.pow(x,2) + Math.pow(y,2))

结束

所以现在把它全部清理成一个有用的包

这有

  • lineHelper.distPoint2Line(px,py,l1x,l1y,l2x,l2y)返回点(px,py)离线的距离((l1x,l1y),(l2x, L 2 Y))
  • 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 点。返回的值将始终为偶数或0。
  • lineHelper.getPointOnLine()作为上述计算的结果,该行上的点被存储并可以检索 通过这个电话。它将返回一个由两个数字组成的数组 线段上的坐标即[x,y]。对于线路这一点可能 在线段之外。对于线条,这一点将在线上 或者在起点或终点。如果在结束点它可能会被淘汰 这类数学中固有的浮点误差。使用 Math.EPSILON检查这是在结尾还是以下 用于查看单位距离是否为1
  • lineHelper.getUnitDist()作为计算的结果,返回沿线的最近点的单位距离 在计算中给出的点。它被夹紧用于线段和 没有线条。如果线/线段是它的一个点 是0.这对indexOfLineClosest2Point而言可能无效 是任何价值。
  • lineHelper.getMinDist()作为函数indexOfLineClosest2Point的结果,此函数将返回到该函数找到的线段的距离。该值仅在调用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; },
   }
})();