We are creating a system for simulating various processes, in which the user can describe and see how a particular process works with the help of visual programming. In other words, to check how the result of the process is affected by certain causeandeffect relationships. The entire system is built on nodes – visual representations of functions that receive, process, show, and eventually send data to the next nodes.
I can think of only two ways to represent the connections between the nodes in a way that is clear and beautiful:

Polylines with right angles, just like in UML diagrams. This type of connection is good when we need to show clear hierarchies and relationships between the objects being connected, for which, often, it does not matter where this connection comes from. In the real world, this could resemble a pipeline, with various branches and intersections, that connects the tanks.

Smooth curves like Nodes in UE4 or Shader Nodes in Blender. They clearly show not only the relationships between objects, but also their interactions, as well as define specific inputs and outputs for different data. In turn, these connections can be thought of as wires in an analog modular synthesizer that connect sound generators and many filters to each other to extract a unique sound.
The second option looks ideal for solving the problem – our nodes may not have a clear structure and hierarchy, they may have several inputs and outputs, but the interaction between them is strictly limited by the types of input and output data. So how do you draw these connections beautifully?
Realization
Since our application doesn’t use canvas, the solution should also use the DOM to display connections. The first candidate for drawing curves is <path />
in SVG.
Below is a rough idea of what the main space in which the work takes place looks like:
<div class="container">
<div class="nodescontainer">
<!...>
</div>
</div>
.container {
position: absolute;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
overflow: hidden;
}
.nodescontainer {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
transformorigin: left top;
/*
Задается динамически, но здесь и далее для удобства
будут использованы эти значения
*/
transform: translate(640px, 360px) scale(0.5);
}
Put <svg />
in the DOM above nodescontainer so that it is rendered first and is below. We will also throw some styles on it so that it occupies all the space and does not intercept events, and inside we will wrap all the connections in <g />
for synchronization transform
with .nodescontainer
.
<div class="container">
<svg class="connectionscontainer">
<g transform="translate(640, 360) scale(0.5)">
<!...>
</g>
</svg>
<div class="nodescontainer">
<!...>
</div>
</div>
.container {
/* ... */
}
.nodescontainer {
/* ... */
}
.connectionscontainer {
pointerevents: none;
overflow: hidden;
position: absolute;
width: 100%;
height: 100%;
transformorigin: left top;
}
At this point, you’re done with the preparation and you can move on to rendering the connections themselves. First, let’s connect the ports with straight lines to figure out their positioning. The <path/>
there is an attribute d
, which describes the geometry of the shape. For a straight line, two commands are enough – “Move to” – M
and “Line to” – L
. The first one indicates the point from which the drawing of the shape begins, the second one draws a line to the next point. Both commands have the following syntax:
M x, y
L x, y
We know port centers in the format of {x, y}
, so to connect the {x: 20, y: 60}
and { x: 45, y: 90 }
expression d
will look like:
M 20, 60 L 45, 90
<path />
You’ll add a few more properties to avoid filling the shape, as well as specify the color and thickness of the line itself:
<path
d="M 20 60 L 45 90"
fill="transparent"
stroke="rgba(0, 0, 0, 0.2)"
strokewidth="1"
></path>
Now it’s time to add beauty and make natural bend the resulting lines for cases where the ports are at different heights. To do this, we will use a bundle of two quadratic Bézier curves. As a result, we should get a curve that will resemble the letter S in shape, since the node ports can be on the left and right, but not on the top or bottom. A quadratic Bézier curve is defined by three control points P₀ (initial), P₁ (control) and P₂ (finite), and its equation is as follows:
To display such a curve in d, use the Q command with the arguments P₁ and P₂. In turn, the P₀ is defined by the previous command of the expression d, which in this case is M , which indicates the point of origin of the figure. In this way, half of the required line is obtained.
M x0, y0 Q x1, y1 x2, y2
In order to draw the second half, which is the same curve reflected horizontally, it is enough to use the command T. This command takes only one point as an argument P₂ for the equation. P₀ for it is the endpoint of the preceding curve, and P₁ is calculated as a reflection of the previous control point relative to the current one P₀. In other words, the line continues as a reflection of the previous Bézier curve to the specified point.
M x0, y0 Q x1, y1 x2, y2 T x3, y3
Let’s write a function to generate the necessary expression d
. We Know the Points {x0, y0}
and {x3, y3}
are the coordinates of the output and input ports. Dot {x2, y2}
– will be the center of a straight line between these two points.
type Point = {
x: number,
y: number
};
function calculatePath(start: Point, end: Point) {
const center = {
x: (start.x + end.x) / 2,
y: (start.y + end.y) / 2,
};
return `
M ${start.x},${start.y}
Q x1, y1 ${center.x},${center.y}
T ${end.x},${end.y}
`;
}
The only thing left to do is to calculate the checkpoint {x1, y1}
. To do this, we will shift the start point of the line along the axis X. The original y should be left so that the line tends to a horizontal position at the entry and exit points. To calculate the displacement, take the minimum of the distance between the points start
and end
, half the distance along the axis Y, as well as a limit of 150 to avoid excessive stretching of the curve at large distances of nodes from each other.
type Point = {
x: number,
y: number
}
function distance(start: Point, end: Point)
{
const dx = start.x  end.x
const dy = start.y  end.y
return Math.sqrt(dx * dx + dy * dy)
}
function calculatePath(start: Point, end: Point) {
const center = {
x: (start.x + end.x) / 2,
y: (start.y + end.y) / 2,
}
const controlPoint = {
x: start.x + Math.min(
distance(start, end),
Math.abs(end.y  start.y) / 2,
150
),
y: start.y,
};
return `
M ${start.x},${start.y}
Q ${controlPoint.x}, ${controlPoint.y} ${center.x},${center.y}
T ${end.x},${end.y}
`;
}
With this checkpoint calculation, if the ports are at the same height, the line will be straight, but will curve in proportion to the distance of the nodes from each other.
Conclusion
This method of rendering a connection is valid for nodes whose ports are located on opposite sides. However, for ports that are on the same side, you can use cubic Bézier curves by adding the same calculation of the second control point, which will use the offset from the end of the control point.
Thank you for reading this article, I hope you found it interesting!
———
Acknowledgment and Usage Notice
The editorial team at TechBurst Magazine acknowledges the invaluable contribution of Nikita Mizev the author of the original article that forms the foundation of our publication. We sincerely appreciate the author’s work. All images in this publication are sourced directly from the original article, where a reference to the author’s profile is provided as well. This publication respects the author’s rights and enhances the visibility of their original work. If there are any concerns or the author wishes to discuss this matter further, we welcome an open dialogue to address potential issues and find an amicable resolution. Feel free to contact us through the ‘Contact Us’ section; the link is available in the website footer.