Reputation: 8970
I have some javascript code that is generating SVG paths to connect HTML elements on a page. This show which box is connected to another box.
The paths are using markers so that I can have an arrow on the end to show its direction.
Overall, this is working well. I have been trying to add a circle
to this setup now that is directly in the center of the starting and ending element.
My goal is that the path stops just short of this circle so that the arrow is pointing to it, not on top of it.
Code Sample:
//helper functions, it turned out chrome doesn't support Math.sgn()
function signum(x) {
return (x < 0) ? -1 : 1;
}
function absolute(x) {
return (x < 0) ? -x : x;
}
/**
* Get the offsets of page elements
* @param el
*/
function getOffset(el) {
const rect = el.getBoundingClientRect();
return {
left: rect.left + window.pageXOffset,
top: rect.top + window.pageYOffset,
bottom: rect.bottom - window.pageYOffset,
width: rect.width || el.offsetWidth,
height: rect.height || el.offsetHeight
};
}
/**
* Draw the path on the SVG using proided coords
* @param svg
* @param path
* @param startX
* @param startY
* @param endX
* @param endY
*/
function drawPath(svg, path, startX, startY, endX, endY, circle) {
// Get the path's stroke width (if one wanted to be really precize, one could use half the stroke size)
const style = getComputedStyle(path);
const stroke = parseFloat(style.strokeWidth);
// Check if the svg is big enough to draw the path, if not, set height/width
if (svg.getAttribute("height") < startY) {
svg.setAttribute("height", startY + 20);
}
if (svg.getAttribute("width") < startX + stroke) {
svg.setAttribute("width", startX + stroke + 20);
}
if (svg.getAttribute("width") < endX + stroke) {
svg.setAttribute("width", endX + stroke + 20);
}
/**
M = moveto
L = lineto
H = horizontal lineto
V = vertical lineto
C = curveto
S = smooth curveto
Q = quadratic Bézier curve
T = smooth quadratic Bézier curveto
A = elliptical Arc
Z = closepath
*/
// Straight line from XY Start to XY End
path.setAttribute(
"d",
"M" + startX + " " + startY + " L" + endX + " " + endY
);
// Show the starting and ending circle
if (circle) {
circle.setAttribute("cx", startX);
circle.setAttribute("cy", startY);
circle.setAttribute("cx", endX);
circle.setAttribute("cy", endY);
}
}
/**
* Calculate the coords for where the line will be drawn
* @param svg
* @param path
* @param startElem
* @param endElem
* @param type
*/
function connectElements(svg, path, startElem, endElem, circle) {
// Define our container
const svgContainer = document.getElementById("svgContainer"),
svgTop = getOffset(svgContainer).top,
svgLeft = getOffset(svgContainer).left,
startCoord = startElem,
endCoord = endElem;
let startX, startY, endX, endY;
// Calculate path's start (x,y) coords
// We want the x coordinate to visually result in the element's mid point
startX =
getOffset(startCoord).left +
getOffset(startCoord).width / 2 -
svgLeft;
startY =
getOffset(startCoord).top +
getOffset(startCoord).height / 2 -
svgTop;
// Calculate path's start (x,y) coords
// We want the x coordinate to visually result in the element's mid point
endX =
getOffset(endCoord).left +
0.5 * getOffset(endCoord).width -
svgLeft;
endY =
getOffset(endCoord).top +
getOffset(endCoord).height / 2 -
svgTop;
// Call function for drawing the path
drawPath(svg, path, startX, startY, endX, endY, circle);
}
function connectAll() {
// Loop over our destinations
for (let i = 0; i < dest.length; i++) {
// Define
const marker = document.createElementNS(
"http://www.w3.org/2000/svg",
"marker"
);
const path = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
const markerPath = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
const defs = document.createElementNS(
"http://www.w3.org/2000/svg",
"defs"
);
const circle = document.createElementNS(
"http://www.w3.org/2000/svg",
"circle"
);
// Set definitions attribute
defs.setAttribute("id", "defs");
// Create our center circle
circle.setAttribute("id", "circle_" + dest[i].linkID + "_" + dest[i].boxID);
circle.setAttribute("cx", "0");
circle.setAttribute("cy", "0");
circle.setAttribute("r", "15");
circle.setAttribute("fill", "red");
// Append our circle
document.getElementById('svg1').appendChild(circle);
// Set up marker (Arrow)
marker.setAttribute("id", "Triangle");
marker.setAttribute("viewBox", "0 0 10 10");
marker.setAttribute("refX", "0");
marker.setAttribute("refY", "5");
marker.setAttribute("markerUnits", "strokeWidth");
marker.setAttribute("markerWidth", "4");
marker.setAttribute("markerHeight", "3");
marker.setAttribute("orient", "auto");
// Append our marker (Arrow)
marker.appendChild(markerPath);
markerPath.setAttribute("d", "M 0 0 L 10 5 L 0 10 z");
markerPath.setAttribute("fill", "#428bca");
// Create our main path
path.setAttribute(
"id",
"path_" + dest[i].linkID + "_" + dest[i].boxID
);
path.setAttribute("class", "path");
path.setAttribute(
"marker-end",
"url(" + window.location + "#Triangle)"
);
// Only create one set of definitions
if (i === 0) {
document.getElementById('svg1').appendChild(defs);
document.getElementById('defs').appendChild(marker);
}
// Append our path to the SVG
document.getElementById('svg1').appendChild(path);
const svg = document.getElementById("svg1"),
p = document.getElementById(
"path_" + dest[i].linkID + "_" + dest[i].boxID
),
startingBox = document.getElementById("box_" + dest[i].boxID),
destinationBox = document.getElementById(
"box_" + dest[i].destinationBoxID
);
// Connect paths
connectElements(
svg,
p,
startingBox,
destinationBox,
circle
);
}
}
// Define our boxes to connect
var dest = [{
"boxID": "16",
"destinationBoxID": "5",
"linkID": "1"
},
{
"boxID": "18",
"destinationBoxID": "1",
"linkID": "9"
},
{
"boxID": "2",
"destinationBoxID": "5",
"linkID": "8"
}
]
// Run
connectAll()
My Attempt:
One of the things I tried was adjusting the final X
coordinate for the path like so:
// Straight line from XY Start to XY End
path.setAttribute(
"d",
"M" + startX + " " + startY + " L" + (endX-30) + " " + endY
);
By telling it to end 30 short, I expected the arrow to not quite go all the way to the mid-point.
Well, this works just fine for horizontal lines but when I have a diagonal, it is putting it -30
to the left of the mid-point as its told to do, when really it needs to account for the direction and adjust the Y
as well.
The horizontal line is fine with the -30
but the diagonal one is not where I want it to be (understanding its where I TOLD it to be).
Here is an example of where I would expect to see the diagonal line ending for the top left box:
How can I go about adjusting the XY
based on the direction of the path or is there more to it than that?
JS Fiddle: http://jsfiddle.net/m4fupk7g/5/
Upvotes: 1
Views: 800
Reputation: 1428
You need to convert your lines to angles and distances, reduce the distances and then convert them back. For example:
let dx = endX - startX;
let dy = endY - startY;
let angle = Math.atan2(dy, dx);
let distance = Math.sqrt(dx * dx + dy * dy) - 20;
let offsetX = Math.cos(angle) * distance + startX;
let offsetY = Math.sin(angle) * distance + startY;
Upvotes: 2