I have two divs, and these two divs have an icon on the border. I want to connect the icons
using an SVG path line.
I have tried some solutions, but for some reason, I cannot seem to manage to make it look nice. The line starts away from the actual div. I believe I have some big problems during the calculations.
My project is in Vue, so I cannot give a proper link to show the reproduction, however, here's the function I am using:
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)
}
}
The end result looks like this:
The divs themselves are draggable, and the line is re-drawn accordingly but it maintains that distance from the ports from where it is supposed to start. How do I make sure that the connection starts from the icon infront of "An ouput Port" and ends properly on the other output port.
Also, what kind of bezier curve do I use so that the path tries to go around the div and not from under it?
Any guidance will be appreciated.
EDIT: I have been tampering around with it, and I added a -100
when calculating the Y-axis. I dont know why it works, I tried different values but 100 is what works the best.
I still would like help regarding the working of this because the -100
feels like a fluke and not a proper solution. Also, my goal would be to get the link to not go under the div
but rather around it, if you have any advice regarding that too, it would be of course appreciated, otherwise I can always try to solve it later.
EDIT (ANSWER): So based on chrwahl's help, I went into detail into how am I calculating my offset.
I ended up using the following code to solve my issue. I just had to end up using the offset relative to the parent
// 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}`
The function getOffsetRelativeToParent
is as followed:
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 }
}
It is difficult to guess that the issue is from the images, but somehow you are not taking into account the offset or the position of the SVG in relation to the area (here I call it the board).
I hope that you can learn something from this example:
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>