If you're drawing the all the shapes using ctx.fill(), you can just call ctx.stroke() before each call to ctx.fill(). This will result in a line of width ctx.lineWidth/2, since half of the line will be covered by the shape itself. However, his won't work for other methods such as ctx.drawImage() or ctx.putImageData(). Please specify how exactly you're drawing these shapes to the canvas to receive some more detailed help.
Edit: I think you can use the solution you already mentioned, you just need to make the non-black part of your image transparent. You can do this by editing the the imageData of the canvas:
var ctx = canvas.getContext("2d");
var imageData = ctx.getImageData(0,0,canvas.width,canvas.height);
for (let i=0;i<imageData.data.length;i+=4){
if (shouldBeTransparent(imageData.data[i],imageData.data[i+1],imageData.data[i+2]){
imageData.data[i+3] = 0;
}
}
ctx.putImageData(imageData,0,0);
function shouldBeTransparent(r,g,b){
return r!=0||g!=0||b!=0;
}
This will make all pixels that are not entirely black transparent, so you can continue with the method you already mentioned.