连接两个 div 的 SVG 贝塞尔路径

SVG Bezier path connecting two divs

提问人:Edd Chang 提问时间:11/17/2023 最后编辑:Edd Chang 更新时间:11/22/2023 访问量:80

问:

我有两个 div,这两个 div 的边框上有一个图标。我想使用 SVG 路径线进行连接。icons

我尝试了一些解决方案,但由于某种原因,我似乎无法让它看起来不错。该行从实际的 div 开始。我相信我在计算过程中遇到了一些大问题。

我的项目在 Vue 中,所以我无法提供适当的链接来显示复制,但是,这是我正在使用的功能:

function drawConnector() {
  if (divA.value && divB.value && svg.value) {
    const paths = svg.value

    let oldPaths = paths.children
    for (let a = oldPaths.length - 1; a >= 0; a--) {
      paths.removeChild(oldPaths[a])
    }

    let x1, y1, x4, y4, dx, x2, x3, path, boxA, boxB
    
    const bezierWeight = 0.8

    for (let a = 0; a < 1; a++) {
      // divA and divB are the icons
      boxA = divA.value
      boxB = divB.value
      
      x1 = getOffset(boxA).left + boxA.getBoundingClientRect().width / 2
      y1 = getOffset(boxA).top + (boxA.getBoundingClientRect().height - 100) / 2 // EDIT -100
      x4 = getOffset(boxB).left + boxB.getBoundingClientRect().width / 2
      y4 = getOffset(boxB).top + (boxB.getBoundingClientRect().height - 100)/ 2 // EDIT -100

      // Apply padding

      dx = Math.abs(x4 - x1) * bezierWeight

      if (x4 < x1) {
        x2 = x1 - dx
        x3 = x4 + dx
      } else {
        x2 = x1 + dx
        x3 = x4 - dx
      }

      const data = `M${x1} ${y1} C ${x2} ${y1} ${x3} ${y4} ${x4} ${y4}`
      path = document.createElementNS(SVG_URL, 'path')
      path.setAttribute('d', data)
      path.setAttribute('class', 'path')
      paths.appendChild(path)
    }
  }
}

function getOffset(element: HTMLDivElement) {
  if (!element.getClientRects().length) {
    return { top: 0, left: 0 }
  }

  let rect = element.getBoundingClientRect()
  let win = element.ownerDocument.defaultView
  return {
    top: rect.top + (win ? win.scrollY : 0),
    left: rect.left + (win ? win.scrollX : 0)
  }
}

最终结果如下所示:

enter image description here

div 本身是可拖动的,并且会相应地重新绘制线,但它会保持与应该开始的端口的距离。如何确保连接从“输出端口”前面的图标开始,并在另一个输出端口上正确结束。

另外,我应该使用什么样的贝塞尔曲线才能使路径尝试绕过 div 而不是从它下面?

任何指导将不胜感激。

编辑:我一直在篡改它,我在计算 Y 轴时添加了一个。我不知道它为什么有效,我尝试了不同的值,但 100 是效果最好的。-100

Output after the -100

我仍然希望得到有关其工作的帮助,因为感觉像是侥幸而不是适当的解决方案。此外,我的目标是让链接不要在它下面,而是在它周围,如果您对此有任何建议,当然将不胜感激,否则我以后可以随时尝试解决它。-100div

编辑(回答):因此,根据 chrwahl 的帮助,我详细介绍了如何计算我的偏移量。

我最终使用以下代码来解决我的问题。我最终只需要使用相对于父项的偏移量

// calculate position of source port
        const sourcePosition = {
          x: getOffsetRelativeToParent(divA).left,
          y: getOffsetRelativeToParent(divA).top + divA.offsetHeight / 2
        }

        // calculate position of target port
        const targetPosition = {
          x: getOffsetRelativeToParent(divB).left + 2,
          y: getOffsetRelativeToParent(divB).top + divB.offsetHeight / 2
        }

        // create svg path based on co-ordinates
        const path = document.createElementNS(SVG_URL, 'path')
        path.setAttribute(
          'd',
          `M ${sourcePosition.x}, ${sourcePosition.y} C ${sourcePosition.x + 100}, ${
            sourcePosition.y
          } ${targetPosition.x - 200}, ${targetPosition.y} ${targetPosition.x}, ${targetPosition.y}`

功能如下:getOffsetRelativeToParent

function getOffsetRelativeToParent(child: HTMLDivElement | HTMLElement) {
  const parent = mainDiv.value

  if (!parent) {
    return { top: 0, left: 0 }
  }

  const childRect = child.getBoundingClientRect()
  const parentRect = parent.getBoundingClientRect()

  const top = childRect.top - parentRect.top
  const left = childRect.left - parentRect.left

  return { top, left }
}

现在它看起来像:enter image description here

JavaScript CSS SVG

评论

0赞 AKX 11/17/2023
我认为你可能有 100px 的边距/填充,你没有考虑到某个地方......
0赞 Edd Chang 11/17/2023
图标本身具有左或右(x 轴)的填充,具体取决于输入或输出。但这是为了实现父母边界的重叠。就 y 轴而言,我没有任何内容

答:

3赞 chrwahl 11/19/2023 #1

很难猜测问题出在图像上,但不知何故,您没有考虑 SVG 相对于该区域的偏移量或位置(这里我称之为板)。

我希望你能从这个例子中学到一些东西:

const board = document.getElementById('board');
const svg = board.querySelector('svg');
const cardProps = {'width': 120, 'height': 80, 'offset': 65};

updatePaths();

function updatePaths() {
  let paths = svg.querySelectorAll('path');
  [...paths].forEach(path => {
    let from = board.querySelectorAll(`div.card`)[path.dataset.from];
    let to = board.querySelectorAll(`div.card`)[path.dataset.to];
    let fromPoint = {
      'left': from.offsetLeft + cardProps.width,
      'top': from.offsetTop + cardProps.offset
    };
    let toPoint = {
      'left': to.offsetLeft,
      'top': to.offsetTop + cardProps.offset
    };

    path.setAttribute('d', `M ${fromPoint.left} ${fromPoint.top}
    C ${fromPoint.left + 50} ${fromPoint.top} ${toPoint.left - 50} ${toPoint.top} ${toPoint.left} ${toPoint.top}`);
  });
}

board.addEventListener('mousedown', e => {
  switch (e.target.className) {
    case 'card':
      e.target.dataset.moving = "on";
      e.target.mouse = {
        left: e.layerX,
        top: e.layerY
      };
      break;
  }
});

board.addEventListener('mousemove', e => {
  let card = board.querySelector('div[data-moving="on"]');
  if (card) {
    card.style.left = `${e.clientX - card.mouse.left - board.offsetLeft}px`;
    card.style.top = `${e.clientY - card.mouse.top - board.offsetTop}px`;
    updatePaths();
  }
});

document.addEventListener('mouseup', e => {
  let cards = board.querySelectorAll('div[data-moving]');
  [...cards].forEach(card => card.dataset.moving = null);
});
#board {
  width: 100%;
  height: 400px;
  border: solid thin navy;
  position: relative;
}

.card {
  border: solid thin black;
  width: 120px;
  height: 80px;
  position: absolute;
  background-color: WhiteSmoke;
}

.card::before {
  content: "";
  width: 10px;
  height: 10px;
  border-radius: 5px;
  background-color: gray;
  position: absolute;
  left: -5px;
  top: 60px;
}

.card::after {
  content: "";
  width: 10px;
  height: 10px;
  border-radius: 5px;
  background-color: gray;
  position: absolute;
  right: -5px;
  top: 60px;
}

svg path {
  stroke-width: 2px;
  fill: none;
}
<div id="board">
  <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%">
    <path d="M 0 0 L 20 20" stroke="black" data-from="0" data-to="1" />
</svg>
  <div class="card" style="left: 10px;top: 10px"></div>
  <div class="card" style="left: 200px;top: 50px"></div>
</div>