I have an HTML canvas that I am drawing stock ticker candles on. When displayed with a window.devicePixelRatio
of 1, everything is fine; I make sure to render on whole pixel values so that all the rectangles, and lines are drawn cleanly:
When looking at this when the window.devicePixelRatio
is not 1, the method of just using Math.floor
for the pixel value no longer works, as the window.devicePixelRatio
can be a factional value.
For example, when looking at this on a 4k monitor, with a window.devicePixelRatio
of 1.5, the candles render as such:
As you can see, those same candles now have aliasing around their edges which I would like to get rid of. I have narrowed it down (I think) that in order to render an edge without aliasing, the pixel value provided multiplied by the pixel ratio needs to be a whole number: ie. providing an x pixel of 1 would not work, because, 1 * 1.5 = 1.5
, but a value of 2 would, because, 2 * 1.5 = 3
which is a whole value.
In the picture below, the left candle has an x coordinate of 2, whereas the right candle has an x coordinate of 9. 2 * 1.5 = 3
, but 9 * 1.5 = 13.5
so it gets aliased.
Is there a simple general mathematical solution to be able to compute what pixel value I should provide when I call fillRect
, stroke
, etc so that I won't get this aliasing? Ie. a function where if I passed in 9
, I would get back 10
if I had a window.devicePixelRatio
of 1.5; basically rounding up to nearest x pixel value that wouldn't give me aliasing.
Edit code reference for how I am scaling the canvas:
this._canvas.nativeElement.style.width = `${this._width}px`;
this._canvas.nativeElement.style.height = `${this._height}px`;
const dpr = window.devicePixelRatio;
this._canvas.nativeElement.width = Math.floor(this._width * dpr);
this._canvas.nativeElement.height = Math.floor(this._height * dpr);
this._context.scale(dpr, dpr);
The issue is that you are scaling the whole context through ctx.scale()
. This is indeed the easiest way generally, but this also means that with a non-integer pixel ratio the canvas context will itself convert your integer coordinates to floating ones.
Instead you will want to perform the scaling yourself and round where needed.
const dPR = devicePixelRatio;
const canvas = document.querySelector("canvas");
canvas.width *= dPR;
canvas.height *= dPR;
const ctx = canvas.getContext("2d");
/**
* Scale the coordinate to the current pixel ratio
* Does not round. To be used with methods that don't require
* to fall on integer coords (e.g non straight poly-lines)
*/
const s = (val) => val * devicePixelRatio;
/**
* Scale the coordinate to the current pixel ratio, rounded to the nearest integer.
*/
const r = (val) => Math.round(s(val));
// Draw a rect using rounded coords
ctx.fillRect(r(50), r(50), r(1), r(75));
canvas { width: 300px; height: 150px; }
<canvas></canvas>