I am creating a frame plugin. The arc seems to be drawn okay as shown here:

But if you look carefully, you will notice the wild edges compared to this idle frame:

So, how can I smooth the edges to match the second image, given this code piece:
function drawFancyArcFrame(mainCtx, opts) {
const {
cx, cy, radius, width, startDeg, sweepDeg,
color1, color2, capStyle = 'feather',
capLengthPx = 24, pixelSize = 4
} = opts;
if (sweepDeg <= 0 || width <= 0) return;
const startRad = (startDeg * Math.PI) / 180;
const endRad = ((startDeg + sweepDeg) * Math.PI) / 180;
const W = mainCtx.canvas.width;
const H = mainCtx.canvas.height;
const off = document.createElement('canvas');
off.width = W; off.height = H;
const ctx = off.getContext('2d');
const grad = ctx.createLinearGradient(0, 0, W, H);
grad.addColorStop(0, color1);
grad.addColorStop(1, color2);
// draw base arc with flat ends so we can sculpt
ctx.save();
ctx.strokeStyle = grad;
ctx.lineWidth = width;
ctx.lineCap = 'butt';
ctx.globalAlpha = 0.85;
ctx.beginPath();
ctx.arc(cx, cy, radius, startRad, endRad, false);
ctx.stroke();
ctx.restore();
// endpoints + tangents
const sx = cx + radius * Math.cos(startRad);
const sy = cy + radius * Math.sin(startRad);
const ex = cx + radius * Math.cos(endRad);
const ey = cy + radius * Math.sin(endRad);
const tStart = startRad + Math.PI / 2;
const tEnd = endRad + Math.PI / 2;
// erasers (destination-out)
const eraseFeather = (x, y, ang) => {
ctx.save();
ctx.translate(x, y);
ctx.rotate(ang);
ctx.globalCompositeOperation = 'destination-out';
const g = ctx.createLinearGradient(0, 0, capLengthPx, 0);
g.addColorStop(0, 'rgba(0,0,0,1)');
g.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = g;
ctx.fillRect(0, -width/2, capLengthPx, width);
ctx.restore();
};
const eraseChop = (x, y, ang) => {
ctx.save();
ctx.translate(x, y);
ctx.rotate(ang);
ctx.globalCompositeOperation = 'destination-out';
ctx.beginPath();
ctx.moveTo(0, -width/2);
ctx.lineTo(capLengthPx, 0);
ctx.lineTo(0, width/2);
ctx.closePath();
ctx.fillStyle = 'rgba(0,0,0,1)';
ctx.fill();
ctx.restore();
};
const erasePixel = (x, y, ang) => {
ctx.save();
ctx.translate(x, y);
ctx.rotate(ang);
ctx.globalCompositeOperation = 'destination-out';
const cols = Math.ceil(capLengthPx / pixelSize);
const rows = Math.ceil(width / pixelSize);
const y0 = -width / 2;
for (let c = 0; c < cols; c++) {
const frac = 1 - (c / cols);
const activeRows = Math.max(1, Math.floor(rows * frac));
const x0 = c * pixelSize;
for (let r = 0; r < activeRows; r++) {
if (Math.random() < 0.85) {
const yCell = y0 + r * pixelSize;
ctx.fillStyle = 'rgba(0,0,0,1)';
ctx.fillRect(x0, yCell, pixelSize, pixelSize);
}
}
}
ctx.restore();
};
switch (capStyle) {
case 'feather':
eraseFeather(sx, sy, tStart);
eraseFeather(ex, ey, tEnd + Math.PI);
break;
case 'chop':
eraseChop(sx, sy, tStart);
eraseChop(ex, ey, tEnd + Math.PI);
break;
case 'pixel':
erasePixel(sx, sy, tStart);
erasePixel(ex, ey, tEnd + Math.PI);
break;
}
mainCtx.drawImage(off, 0, 0);
}
I want to remove the straight-edge seams to match the second image’s smooth fading.

