Opacity is how a canvas creates the appearance of lines narrower than a pixel, because a pixel is (by definition) the smallest unit a canvas works with.
That doesn't mean all hope is lost, however, because this is only a problem depending on what you mean by "pixel".
There's the CSS unit px, which corresponds to what used to be the smallest actual pixels available on video display devices, typically in the range of 72-96 per inch (28-38 per cm).
And then there are actual device pixels, which are now often half as small or smaller.
The trick to getting sharper lines with a canvas when you've got high-res device pixels is scaling. Use this to figure how much you can effectively scale:
const scaling = window.devicePixelRatio || 1;
Suppose the value here is 2 (as it is on my current laptop). If you want to create a canvas that occupies, say, 400px by 300px, create an 800x600 canvas. Then use CSS styling width: 400px; height: 300px on your canvas.
Now you'll be able to create sharp half-width lines so long as the device you're drawing to supports the higher resolution.
I use this trick many places in the Angular app at https://skyviewcafe.com/. There's a link there to the source code if you want to dig through it to find examples of high-res canvas drawing.
Please note!
You're either have to specify a lineWidth of 1 when you mean half-width, or use canvas scaling like this:
const context = this.canvas.getContext('2d');
context.scale(scaling, scaling);
Be careful with context.scale() — its effects are cumulative. If you execute context.scale(2, 2) twice in a row with the same context, your scaling factor will be 4, not 2.
context.setTransform(1, 0, 0, 1, 0, 0);
...resets the scaling factor if you want to call context.scale() more than once, without the effect being cumulative.