The solution I came up with involves using the SVG Q
command, which draws a quadratic bezier curve. The main syntax, as it appears in a d
attribute, is:
Q x,y px,py
where x,y
is the control point of the curve and px,py
is the end point of the curve. I find this picture to be helpful:

So in order for this curve to look like a natural continuation of the two sides of the elbow, the derivative at the start and end point of the curve must be equal to the slope of the two straight lines.
Let's consider a <path>
that forms a sharp corner at P(145,75):
<path d="M 20,40 L 145,75 L 35,175.5" stroke="black"/>
Following the picture above, that corner is then the control point of the quadratic curve. You now want to stop the straight line L
at a certain distance from the vertex, calculated from user input, and that will be the start point of the quadratic bezier. The end point will be the px,py
shown above.
Therefore we can modify our path like this (with string interpolation):
<path d=`M 20,40 L ${bezierStart} Q ${vertex} ${bezierEnd} L 35,175.5` stroke="black"/>
Graphically:

In order to calculate all this you only need the three points that identify the triangle rooted at the vertex you want to round. The calculation is based on this Math.SE post except that the d/dt
ratio mentioned over there in this case is already known: it comes from the user.
In my case, I let the user input integers as if they were setting a border-radius
property, and then divide by 100, so that the user input translates to an offset from the vertex point.
The function is as follows (in Typescript):
type Point = [number,number]
type QBezierAnchors = {
vertex: Point
start: Point
end: Point
}
// p: corner to round off
// a1: left anchor
// a2: right anchor
// as if the polygonal shape is drawn from a1 -> p -> a2
// r: user-supplied radius
export function quadraticBezier(p: Point, a1: Point, a2: Point, r: number): QBezierAnchors {
// the corner to round is also the control point of the quadratic bezier
const ctrl = p
// formula for finding point p at a certain distance from p0 on a line that passes through p1
// px = (1-t)p0x + tp1x
// py = (1-t)p0y + tp1y
// t is ratio of distances d/dt where d = distance(p0,p1) and dt = distance(p0,p)
// but in our case we already know the ratio because it's set by the user
const t = r / 100
const start = [((1-t)*p[0] + (t*a1[0])), ((1-t)*p[1] + (t*a1[1]))]
const end = [((1-t)*p[0] + (t*a2[0])), ((1-t)*p[1] + (t*a2[1]))]
return {
vertex: ctrl,
start,
end
}
}
Then just format the output of this function into the <path>
element as shown above.
Runnable example:
function initialize() {
document.querySelectorAll(".myInput").forEach((v) => {
v.addEventListener("keyup", update)
})
update()
}
function update() {
const startX = document.getElementById("startX").value
const startY = document.getElementById("startY").value
const vX = document.getElementById("vX").value
const vY = document.getElementById("vY").value
const endX = document.getElementById("endX").value
const endY = document.getElementById("endY").value
const radius = document.getElementById("userRadius").value
const q = quadraticBezier([vX, vY], [startX, startY], [endX, endY], radius)
const path = document.getElementById("thePath")
path.setAttribute("d", `M ${startX},${startY} L ${q.start} Q ${q.ctrl} ${q.end} L ${endX},${endY}`)
}
function quadraticBezier(p, a1, a2, r) {
const ctrl = p
const t = r / 100
const start = [((1 - t) * p[0] + (t * a1[0])), ((1 - t) * p[1] + (t * a1[1]))]
const end = [((1 - t) * p[0] + (t * a2[0])), ((1 - t) * p[1] + (t * a2[1]))]
return {
ctrl,
start,
end
}
}
initialize()
.myInput {
margin-left: 5px;
margin-top: 5px;
}
<div>
<label>Start x</label><input id="startX" class="myInput" value="20" />
<label>y</label><input id="startY" class="myInput" value="40" />
</div>
<div>
<label>Vertex x</label><input id="vX" class="myInput" value="145" />
<label>y</label><input id="vY" class="myInput" value="75" />
</div>
<div>
<label>End x</label><input id="endX" class="myInput" value="35" />
<label>y</label><input id="endY" class="myInput" value="175" />
</div>
<div>
<label>Radius</label><input id="userRadius" class="myInput" value="10" />
</div>
<div>
<svg width="240" height="320" viewBox="0 0 240 320" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="thePath" stroke="black" stroke-width="4"/>
</svg>
</div>