For a web application, I need to display small images as a circle and draw a circular gradient-filled border around them using only HTML and CSS. For unknown reasons, some systems reproducibly show misalignment between the image and the border so that they are not concentric. On affected systems, this behavior is visible in both Chrome, Firefox, and Edge, however, the direction of the misalignment is different. Other systems, however, are perfectly fine.
Enlarged screenshot of the misalignment in my web application:
My first thought was, that this might be a subpixel rendering issue but since I am using an even-numbered image size of 24x24px and an even-numbered border width of 2px this seems unlikely. I did some experiments by gradually increasing the image size by 1px and found that the direction and extent of misalignment are inconsistent and sometimes there seems to be an oval distortion. Below you find a reduced code snippet at screenshots from Chrome, Firefox, and Edge. I indicated the direction of misalignment in red. Increasing the border width yielded similar results, but the effect seems most pronounced with 2px.
.rounded-corners-gradient-borders {
box-sizing: border-box;
padding: 2px;
border-radius: 100%;
background: linear-gradient(45deg, #F48ACE 0%, #493A97 100%);
}
<img class="rounded-corners-gradient-borders" src="https://i.picsum.photos/id/368/24/24.jpg?hmac=qTESgqsVn81m_y-i5SDjG0xncWcyf-gYC40Jw9amc-k" />
https://codepen.io/grilly17/pen/VwXNrMO
Annotated screenshot of Codepen output in Firefox:
Annotated screenshot of Codepen output in Chrome:
I am aware that drawing a perfectly concentric "solid colored" border can be achieved a lot easier, but the gradient is a hard requirement in this case.
Since it doesn't seem to affect all systems, I asked friends and colleagues to have a look at different OS types, OS versions, browsers, browser versions, monitors, screen resolutions, and different compute hardware but I was not able to find a common cause for this. The direction and extent of misalignment seemed to be different on every system and browser but it does not change when reloading the page in the same browser again. So it appears to be deterministic.
At this point, my best guess is that it is related to some rounding error in the rendering process, but I would love to get to the bottom of this. Does anybody know why this is happening at all and why it is only affecting some systems? Is there a better solution to achieve this?
Thanks to the hint of "CSS pixels vs screen pixels" I was able to understand the root cause and find a solution to my problem. I should have realized that the screenshot of the icon was 35px high instead of the expected 28px including padding.
Most OS have a display setting for "scaling" up everything on your screen by a certain factor, e.g. 125%. This affects everything on your screen and may cause fractional pixel values, which results in the effect described above. If you have multiple screens, the value might be different on every screen. For web applications, the active screen's scaling value is applied only on page loading/rendering and not when moving the page between screens.
The scaling factor can be accessed via the JavaScript window property window.devicePixelRatio.
Using this I was able to work out two acceptable solutions, which might be useful for others:
The enlarged screenshot below shows from left to right the original misaligned image, the "device pixel perfect" image, and the "no subpixel" image when using a display scaling of 125%.
Here is my code (tested on FF, Chrome, Edge): https://codepen.io/grilly17/pen/QWmegPj
function precompensateScaling(value, scale) {
return value / scale;
}
function precompensatePixelFractions(value, scale) {
return value - value * scale % 1 / scale;
}
// wait until page is fully loaded
window.onload = (event) => {
const original = document.getElementById('original');
const oHeight = parseFloat(window.getComputedStyle(original).getPropertyValue('height'));
const oPadding = parseFloat(window.getComputedStyle(original).getPropertyValue('padding'));
const scale = window.devicePixelRatio;
const unscaled = document.getElementById('unscaled');
//unscaled.style.transform = `scale(${1/scale})`; // alternative
unscaled.style.height = `${precompensateScaling(oHeight, scale)}px`;
unscaled.style.padding = `${precompensateScaling(oPadding, scale)}px`;
const adjusted = document.getElementById('adjusted');
adjusted.style.height = `${precompensatePixelFractions(oHeight, scale)}px`;
adjusted.style.padding = `${precompensatePixelFractions(oPadding, scale)}px`;
};
Thank you all for your support. I <3 the Stack Overflow community!