KonvaJS 连接正方形并正确放置线条?

KonvaJS connect squares and correct line placement?

所以我正在用 KonvaJS 和 KonvaReact 构建一个 UML 绘图工具,为此我需要用线条连接形状。我在网站上看到了有关连接对象的教程 https://konvajs.org/docs/sandbox/Connected_Objects.html

他们使用函数 get_connecter_points 根据圆上的弧度计算直线的位置。

function getConnectorPoints(from, to) {
        const dx = to.x - from.x;
        const dy = to.y - from.y;
        let angle = Math.atan2(-dy, dx);

        const radius = 50;

        return [
          from.x + -radius * Math.cos(angle + Math.PI),
          from.y + radius * Math.sin(angle + Math.PI),
          to.x + -radius * Math.cos(angle),
          to.y + radius * Math.sin(angle)
        ];
      }

我正在尝试想出一个模拟函数,但无法想出一个好的解决方案或找到一个好的例子。正如您在图像中看到的那样,我刚刚在函数中返回了 from x 和 y 以及 to x 和 y,因此这些线将放置在每个正方形的左上角。

该函数的目标应该是将线放置在正方形一侧的中间位置,并位于正方形的正确一侧。所以当 to 正方形放在下面时,它应该出现在底部。

因此,如果有人有解决方案,我们将不胜感激。

对于矩形,数学运算比圆形复杂一些。

首先,您需要计算两个对象之间连接线的角度:

function getCenter(node) {
  return {
    x: node.x() + node.width() / 2,
    y: node.y() + node.height() / 2
  }
}
const c1 = getCenter(object1);
const c2 = getCenter(object2;

const dx = c1.x - c2.x;
const dy = c1.y - c2.y;
const angle = Math.atan2(-dy, dx);

其次,当您知道角度后,您需要一个函数,它可以找到矩形边框的一个点,您可以用它来连接另一个对象。

function getRectangleBorderPoint(radians, size, sideOffset = 0) {
  const width = size.width + sideOffset * 2;

  const height = size.height + sideOffset * 2;

  radians %= 2 * Math.PI;
  if (radians < 0) {
    radians += Math.PI * 2;
  }

  const phi = Math.atan(height / width);

  let x, y;
  if (
    (radians >= 2 * Math.PI - phi && radians <= 2 * Math.PI) ||
    (radians >= 0 && radians <= phi)
  ) {
    x = width / 2;
    y = Math.tan(radians) * x;
  } else if (radians >= phi && radians <= Math.PI - phi) {
    y = height / 2;
    x = y / Math.tan(radians);
  } else if (radians >= Math.PI - phi && radians <= Math.PI + phi) {
    x = -width / 2;
    y = Math.tan(radians) * x;
  } else if (radians >= Math.PI + phi && radians <= 2 * Math.PI - phi) {
    y = -height / 2;
    x = y / Math.tan(radians);
  }

  return {
    x: -Math.round(x),
    y: Math.round(y)
  };
}

现在,您只需要为线形生成点:

function getPoints(r1, r2) {
  const c1 = getCenter(r1);
  const c2 = getCenter(r2);

  const dx = c1.x - c2.x;
  const dy = c1.y - c2.y;
  const angle = Math.atan2(-dy, dx);

  const startOffset = getRectangleBorderPoint(angle + Math.PI, r1.size());
  const endOffset = getRectangleBorderPoint(angle, r2.size());

  const start = {
    x: c1.x - startOffset.x,
    y: c1.y - startOffset.y
  };

  const end = {
    x: c2.x - endOffset.x,
    y: c2.y - endOffset.y
  };

  return [start.x, start.y, end.x, end.y]
}

function updateLine() {
  const points = getPoints(rect1, rect2);
  line.points(points);
}

所有这些作为演示:

function getRectangleBorderPoint(radians, size, sideOffset = 0) {
  const width = size.width + sideOffset * 2;

  const height = size.height + sideOffset * 2;

  radians %= 2 * Math.PI;
  if (radians < 0) {
    radians += Math.PI * 2;
  }

  const phi = Math.atan(height / width);

  let x, y;
  if (
    (radians >= 2 * Math.PI - phi && radians <= 2 * Math.PI) ||
    (radians >= 0 && radians <= phi)
  ) {
    x = width / 2;
    y = Math.tan(radians) * x;
  } else if (radians >= phi && radians <= Math.PI - phi) {
    y = height / 2;
    x = y / Math.tan(radians);
  } else if (radians >= Math.PI - phi && radians <= Math.PI + phi) {
    x = -width / 2;
    y = Math.tan(radians) * x;
  } else if (radians >= Math.PI + phi && radians <= 2 * Math.PI - phi) {
    y = -height / 2;
    x = y / Math.tan(radians);
  }

  return {
    x: -Math.round(x),
    y: Math.round(y)
  };
}

const stage = new Konva.Stage({
  container: 'container',
  width: window.innerWidth,
  height: window.innerHeight
});

const layer = new Konva.Layer();
stage.add(layer);

const rect1 = new Konva.Rect({
  x: 20,
  y: 20,
  width: 50,
  height: 50,
  fill: 'green',
  draggable: true
});
layer.add(rect1);


const rect2 = new Konva.Rect({
  x: 220,
  y: 220,
  width: 50,
  height: 50,
  fill: 'red',
  draggable: true
});
layer.add(rect2);

const line = new Konva.Line({
  stroke: 'black'
});
layer.add(line);

function getCenter(node) {
  return {
    x: node.x() + node.width() / 2,
    y: node.y() + node.height() / 2
  }
}

function getPoints(r1, r2) {
  const c1 = getCenter(r1);
  const c2 = getCenter(r2);

  const dx = c1.x - c2.x;
  const dy = c1.y - c2.y;
  const angle = Math.atan2(-dy, dx);

  const startOffset = getRectangleBorderPoint(angle + Math.PI, rect1.size());
  const endOffset = getRectangleBorderPoint(angle, rect2.size());

  const start = {
    x: c1.x - startOffset.x,
    y: c1.y - startOffset.y
  };

  const end = {
    x: c2.x - endOffset.x,
    y: c2.y - endOffset.y
  };
  
  return [start.x, start.y, end.x, end.y]
}

function updateLine() {
  const points = getPoints(rect1, rect2);
  line.points(points);
}

updateLine();
layer.on('dragmove', updateLine);

layer.draw();
  <script src="https://unpkg.com/konva@^3/konva.min.js"></script>
  <div id="container"></div>

最简单的方法可能就是拥有两层,一层用于连接矩形中心的线,另一层用于在线的顶部绘制矩形。

除此之外,我想到了另一种方法来解决“矩形上的点问题”,在我看来效果不错。

从这个矩形的隐式公式开始(我从here得到它,但我认为你也可以通过旋转\left| x \right| + \left| y \right| = 1的坐标系并拉伸垂直和水平轴来推导它):

\left| \frac{x}{w} + \frac{y}{h} \right| + \left| \frac{x}{w} - \frac{y}{h} \right| = 1

我们可以通过代入x = r \cos{\theta}y = r \sin{\theta}来转换为极坐标。重写为半径的函数得到:

r = \frac{1}{\left| \frac{\cos{\theta}}{w} + \frac{\sin{\theta}}{h} \right| + \left| \frac{\cos{\theta}}{w} - \frac{\sin{\theta}}{h} \right|}

然后我们可以使用相同的替换将其转换回直角坐标并得到 θ 的参数函数:

x(\theta) = \frac{\cos{\theta}}{\left| \frac{\cos{\theta}}{w} + \frac{\sin{\theta}}{h} \right| + \left| \frac{\cos{\theta}}{w} - \frac{\sin{\theta}}{h} \right|}

y(\theta) = \frac{\sin{\theta}}{\left| \frac{\cos{\theta}}{w} + \frac{\sin{\theta}}{h} \right| + \left| \frac{\cos{\theta}}{w} - \frac{\sin{\theta}}{h} \right|}

那么这有什么用呢?除了我们不必担心角度落在哪个范围这一事实之外,在极坐标中工作使得旋转变得微不足道 - 它只是一个域偏移。

使用 r 的极坐标函数(为了便于表示)我们可以表示为(设 α 为我们逆时针旋转的角度):

x(\theta) = r(\theta - \alpha) \cos{\theta}}

y(\theta) = r(\theta - \alpha) \sin{\theta}}

您可以在此 desmos 可视化中使用它:https://www.desmos.com/calculator/zgi3jzb2eg

将其付诸实践(假设围绕中心旋转),我们得到:

function degreesToRadians(degrees) {
    return Math.PI * degrees / 180;
}


function getPointOnRectangle(width, height, angle, rotation) {
    const rot_angle = angle - rotation;

    const radius = 1 / (
        Math.abs(Math.cos(rot_angle) / width + Math.sin(rot_angle) / height) + Math.abs(Math.cos(rot_angle) / width - Math.sin(rot_angle) / height)
    );

    return {x: radius * Math.cos(angle), y: radius * Math.sin(angle)};
}


function connectRects(rectA, rectB, line, margin=0) {
    const deltaX = rectB.x() - rectA.x();
    const deltaY = rectB.y() - rectA.y();

    const angleA = Math.atan2(deltaY, deltaX);
    const angleB = Math.PI + angleA;

    const rotA = Konva.angleDeg? degreesToRadians(rectA.rotation()) : rectA.rotation();
    const rotB = Konva.angleDeg? degreesToRadians(rectB.rotation()) : rectB.rotation();

    const relA = getPointOnRectangle(rectA.width() + margin, rectA.height() + margin, angleA, rotA);
    const relB = getPointOnRectangle(rectB.width() + margin, rectB.height() + margin, angleB, rotB);

    line.points([
        rectA.x() + relA.x,
        rectA.y() + relA.y,
        rectB.x() + relB.x,
        rectB.y() + relB.y,
    ]);
}

Codepen 演示(改编自 lavrton 的演示)在这里:https://codepen.io/creallfluharty/pen/WNEVjgW