I started by using @Blindman67 's answer, which works pretty well for basic static shapes.
I ran into the problem that when using the arc approach, having two points right next to each other is very different than having just one point. With two points next to each other, it won't be rounded, even if that is what your eye would expect. This is extra jarring if you are animating the polygon points.
I fixed this by using Bezier curves instead. IMO this is conceptually a little cleaner as well. I just make each corner with a quadratic curve where the control point is where the original corner was. This way, having two points in the same spot is virtually the same as only having one point.
I haven't compared performance but seems like canvas is pretty good at drawing Beziers.
As with @Blindman67 's answer, this doesn't actually draw anything so you will need to call ctx.startPath() before and ctx.stroke() after.
/**
* Draws a polygon with rounded corners
* @param {CanvasRenderingContext2D} ctx The canvas context
* @param {Array} points A list of `{x, y}` points
* @radius {number} how much to round the corners
*/
function myRoundPolly(ctx, points, radius) {
const distance = (p1, p2) => Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
const lerp = (a, b, x) => a + (b - a) * x
const lerp2D = (p1, p2, t) => ({
x: lerp(p1.x, p2.x, t),
y: lerp(p1.y, p2.y, t)
})
const numPoints = points.length
let corners = []
for (let i = 0; i < numPoints; i++) {
let lastPoint = points[i]
let thisPoint = points[(i + 1) % numPoints]
let nextPoint = points[(i + 2) % numPoints]
let lastEdgeLength = distance(lastPoint, thisPoint)
let lastOffsetDistance = Math.min(lastEdgeLength / 2, radius)
let start = lerp2D(
thisPoint,
lastPoint,
lastOffsetDistance / lastEdgeLength
)
let nextEdgeLength = distance(nextPoint, thisPoint)
let nextOffsetDistance = Math.min(nextEdgeLength / 2, radius)
let end = lerp2D(
thisPoint,
nextPoint,
nextOffsetDistance / nextEdgeLength
)
corners.push([start, thisPoint, end])
}
ctx.moveTo(corners[0][0].x, corners[0][0].y)
for (let [start, ctrl, end] of corners) {
ctx.lineTo(start.x, start.y)
ctx.quadraticCurveTo(ctrl.x, ctrl.y, end.x, end.y)
}
ctx.closePath()
}