5

给定,我如何在两个圆圈之间画一条箭头线:

  1. 圆心的位置
  2. 圆的半径

我正在使用线条标记svg 对象。

如果我将箭头绘制到圆圈的“中心” - 那么箭头是不可见的。如果我将箭头向后移动太远 - 然后线会显示并隐藏箭头的尖端(这里为了更好的可见性而夸大了): 箭头向后移动

根据要求,这是我的代码的相关位(在 livescript 中):

# Draw an arrow to use for lines
svg.append("svg:defs")
 .append("svg:marker")
  .attr("id", "arrow")
  .attr("viewBox", "0 0 10 10")
  .attr("refX", 27)
  .attr("refY", 5)
  .attr("markerUnits", "strokeWidth")
  .attr("markerWidth", 8)
  .attr("markerHeight", 6)
  .attr("orient", "auto")
  .append("svg:path")
  .attr("d", "M 0 0 L 10 5 L 0 10 z")

svg.append("line")
 .attr "x1" 5 
 .attr "x2" 50 
 .attr "y1" 5 
 .attr "y2" 50
 .style "stroke" "black"
 .attr "stroke-width" 2
 .attr "marker-end" "url(\#arrow)"

或者,这里是工作示例的 JSFiddle(请注意,箭头“坐立不安”,看起来恰到好处):http: //jsfiddle.net/yeQS2/

4

4 回答 4

16

如果我理解正确,您需要找到需要添加到源以到达目标圆的边界的 2D 向量。

伪代码:

d = distance between A and B; // (sqrt((xB-xA)² + (yB-yA)²)).
d2 = d - radius;

ratio = d2 / d;

dx = (xB - xA) * ratio;
dy = (yB - yA) * ratio;

x = xA + dx;
y = yA + dy;
于 2012-11-05T15:12:21.373 回答
8

我有同样的问题,这就是我解决它的方法。对原始小提琴所做的更改:

更改.attr("refX", 27).attr("refX", 0)。这会使箭头超出线的末端。

通过将以下代码添加到“tick”,使用三角法计算线的正确结束位置,考虑箭头:

var arrowheadLength = 8, // from markerWidth
    nodeRadius = 10;
link.each(function(d) {
  var x1 = d.source.x,
      y1 = d.source.y,
      x2 = d.target.x,
      y2 = d.target.y,
      angle = Math.atan2(y2 - y1, x2 - x1);
  d.targetX = x2 - Math.cos(angle) * (nodeRadius + arrowheadLength);
  d.targetY = y2 - Math.sin(angle) * (nodeRadius + arrowheadLength);
});

使用计算的 targetX 和 targetY 链接属性:

.attr("x2", function(d){
  return d.targetX;
}).attr("y2", function(d){
  return d.targetY;
})

这是更新的小提琴

于 2014-06-19T18:27:55.517 回答
4

好的,所以我想我会试一试,并用一些矢量数学来实现它,它更漂亮,结果是可重用的。

一些澄清:

  • “向量”只是两个数字(x 和 y)
  • “坐标”在结构上与向量相同,只是对我们有不同的含义。尽管如此,我们可以运行相同的数学运算。
  • “定位向量”是两个向量(如源和目标)
  • 您可以通过从第二个向量中减去第一个向量来“释放”定位向量(您会得到一个不再锚定在坐标系中的新向量)
  • 使用毕达哥拉斯定理(也称为范数)计算向量的“长度”
  • 向量加法”就是简单地将两个或多个向量的 xs 和 ys 相加,得到一个新向量。
  • 标量乘法”和除法是通过将 x 和 y 与标量相除来完成的
  • 单位向量”是向量除以其长度

假设我们希望它动态工作(“每滴答声”),初始链接调整看起来像这样(我使用的是 coffeescript):

links.attr('x1', ({source,target}) -> source.x)
     .attr('y1', ({source,target}) -> source.y)
     .attr('x2', ({source,target}) -> target.x)
     .attr('y2', ({source,target}) -> target.y)

我们要做的是将源和目标nodeRadius移离圆圈。为此,我们使用向量数学来

  1. 释放定位向量(链接由两个坐标组成,我们想要一个未锚定的向量)
  2. 计算自由向量的单位向量
  3. 一旦我们有了它,我们就可以将它乘以nodeRadius. 这个新向量表示节点中心与其边界之间的距离,与链接的方向相同。
  4. 将向量添加到源坐标,这些新坐标将位于圆的边缘。

好的,所以我们将使用以下函数来执行此操作:

length = ({x,y}) -> Math.sqrt(x*x + y*y)
sum = ({x:x1,y:y1}, {x:x2,y:y2}) -> {x:x1+x2, y:y1+y2}
diff = ({x:x1,y:y1}, {x:x2,y:y2}) -> {x:x1-x2, y:y1-y2}
prod = ({x,y}, scalar) -> {x:x*scalar, y:y*scalar}
div = ({x,y}, scalar) -> {x:x/scalar, y:y/scalar}
unit = (vector) -> div(vector, length(vector))
scale = (vector, scalar) -> prod(unit(vector), scalar)

free = ([coord1, coord2]) -> diff(coord2, coord1)

这可能看起来有点压倒性,它非常紧凑,因为coffeescript允许我们直接在方法签名中解构东西,非常方便!如您所见,还有另一个名为scale. 将步骤 2. & 3 结合起来只是一个方便的功能。

现在让我们尝试为链接源设置新的 x 坐标。请记住:坐标应移动nodeRadius,以便它从圆的边界开始,而不是在圆内。

(d) ->
    # Step 1
    freed = free(d)
    # Step 2
    unit = unit(freed)
    # Step 3
    scaled = prod(unit, nodeRadius)
    # Step 2+3 would be scale(freed, nodeRadius)
    # Step 4, coords are pretty much just vectors,
    # so we just use the sum() function to move the source coords
    coords = sum(d.source, scaled)
    return coords.x

没什么!将所有这些放入tick()函数中,我们得到:

links.attr('x1', ({source,target}) -> sum(source, scale(free([source,target]), nodeRadius)).x)
     .attr('y1', ({source,target}) -> sum(source, scale(free([source,target]), nodeRadius)).y)
     .attr('x2', ({source,target}) -> diff(target, scale(free([source,target]), nodeRadius)).x)
     .attr('y2', ({source,target}) -> diff(target, scale(free([source,target]), nodeRadius)).y)

哦,别忘了从目标坐标中减去,否则你只会让这条线再次变长(即移动它nodeRadius)。

于 2014-01-03T17:39:44.263 回答
2

正如@andsens 所说,您正在做一个简单的矢量操作。

如果你把它包装在一个像样的库中,这可以做得更干净。例如,我使用了漂亮的Sylvester矩阵和向量库。

你基本上计算的是:

V 下标边等于开括号模数 V 减去 R 闭括号乘以单位向量 V

其中v是到目标中心的向量,v edge是到目标边缘的向量,半径为r

您可以轻松做到:

// Assume source and target both have x and y properties
// Assume target has radius property
function path2TargetEdge(source, target){

  // V is the vector from the source to the target's center
  var V = $V([target.x-source.x, target.y-source.y]);

  // Vt is the vector from the source to the edge of the target
  var Vt = V.toUnitVector().multiply(V.modulus() - target.radius);

  return {x: Vt.e(1), y: Vt.e(2) }; // Vectors are 1-indexed
}
于 2014-03-04T02:08:09.047 回答