Renderer is blurry when window zoom level is changed
See original GitHub issuehttps://github.com/xtermjs/xterm.js/issues/985#issuecomment-570037111
Right now the canvas size is changed based on window.devicePixelRatio
, one idea is to disable this type fo scaling (see how VS Code’s minimap doesn’t change when zooming) and then scale manually by applying a multiplier to relevant numbers.
Applies to WebGL and canvas renderers.
Issue Analytics
- State:
- Created 4 years ago
- Reactions:13
- Comments:16 (6 by maintainers)
Top Results From Across the Web
Zooming out makes the image blury - Aseprite
To disable it, go to Edit > Preferences > Experimental, and uncheck "New render engine for sprite editor". After pressing "Apply" it should ......
Read more >Canvas grid gets blurry at different zoom levels - Stack Overflow
right now the thickness of the lines change according to the zooming levels and are sometimes blurry. Help will be greatly appreciated.
Read more >Top 7 Ways to Fix Google Chrome Blurry Font Rendering on ...
3. Change Windows Resolution and Windows Scale. By default, Windows recommends using a 150% scaling setting. However, it can be too low when ......
Read more >How to prevent Chrome from blurring small images when ...
When I'm trying to view pixel art up close, chrome starts blurring the image. I want to make it so that even when...
Read more >Fix Latest Chrome looking zoomed in and blurry - gHacks
Most users who experience the issue on Windows seem to have set the DPI scaling to 125% instead of the default 100% value....
Read more >Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start FreeTop Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
Top GitHub Comments
I took some time to dig into this a bit deeper. @Tyriar Please excuse if I’m repeating what you possibly already know. However, I think this might help other people understand why the issue still exists.
General observations
The last point was the reason for why it was so hard to make the issue reproducible. I had to research the basics about how the renderer works, so let’s take a short tour about:
The canvas element
Both the Canvas and the WebGL renderer rely on HTML’s
canvas
element. This is an area of a webpage which a script can draw content on. Drawing means that the resulting object is a pixel-based image. Thus both renderers produce a pixel image of the terminal. This technology has been chosen for performance reasons.Zooming the canvas element: problem and workaround
When a canvas object is zoomed, it produces aliasing effects, due to the nature of pixel graphics. It’s just the same as zooming into a photo.
The xterm.js renderer works around this using the following steps:
Let’s use the following symbols to describe the behaviour mathematically:
w
: canvas widthz
: zoom factorThe scaled canvas size
w'
is then caluclated as:w'
=w
*z
So the terminal is painted on
w'
and then it is visually scaled down, resulting in its original sizew'
/z
=w
The browser then zooms in:
w
*z
=w'
i.e. the width that we see is exactly the width that we painted.
Why the workaround does not guarantee a crisp presentation
In theory and in a continouus scale the above workaround works great. However, since a screen has a limited number of pixels, we only have a discrete number of possible values for
w
andw'
, which makes us eventually stumble upon rounding errors.Let’s see two examples:
No rounding error
Let’s assume a scaling factor of 125% and a canvas width of 100px:
w'
=w
*z
w'
= 100 * 1.25w'
= 125So the terminal is drawn to a 125px wide canvas. Afterwards it is zoomed down:
w'
/z
=w
125 / 1.25 = 100So the canvas is included into the HTML document at a width of 100px. Note that internally the canvas still has its drawn size of 125px, we’re just displaying it with a reduced width.
The browser then scales the whole site:
w
*z
=w'
100 * 1.25 = 125So in the end the canvas is displayed in the same size it was drawn to, everyhing looks crisp.
Rounding error
This time, let’s assume the same scaling factor of 125% but a canvas width of 95px:
w'
=w
*z
w'
= 95 * 1.25w'
= 118.75Since the canvas has to have a discrete size, we have to round it:
⌊w'⌉
= 119In the HTML document, we’re styling the canvas with it original width of 95, so:
w
= 95The browser, however, does not scale each item individually but the complete webpage in one piece. Thus individual items are not rounded to pixel boundaries, only the complete page is. This makes the resulting width of our canvas after the browser zoom in fact:
w'
=w
*z
w'
= 95 * 1.25w'
= 118.75Result: We have drawn the terminal to a 119px wide canvas, which is now displayed over the width of 118.75px. So even though we tried to make canvas size and resulting display size identical, in fact they are not.
I came across this by analyzing the canvas element in the DOM, where the canvas size and the display size can be directly seen:
(Disclaimer: This is an artificial example in order to match my mathematical example above. In the real world I wasn’t able to produce these exact sizes with xterm.js, but I had several other combinations of canvas width and style width leading to the same result.)
The issue now gets even more clear when we calculate the zoom factor
z'
between the canvas width and the style width:z'
=w'
/⌊w'⌉
z'
= 119 / 95z'
≈ 1,2526315789 ≠ 1.25It is obvious that the scaling factor which we actually used to downscale the canvas to its CSS width is not equal to the scaling factor the browser uses to upscale the site.
It’s exactly these cases where we’re getting the blurry terminal lines. I checked cases without rounding errors (like the 100px and 125% zoom above) and in those cases all characters are displayed equally throughout the whole line.
Possible solution
We could choose the CSS width
w
in a way that the canvas widthw'
does not have to be rounded. If it has to be rounded, we have to add some padding around the DOM element in order to reduce its size slightly untilw
*z
is an integer.This will result in blank spaces around the terminal, the maximum size of which depends on the denominator of the zoom factor
z
.Example: The zoom factor 125% from the above examples can be written as 5/4, so the denominator is 4. The CSS width then has to be integrally divisible by 4 in order to make the canvas width integer as well.
w
= 100px 100 is integrally divisible by 4. No adjustments necessary.w
= 95px 95 is not integrally divisible by 4. Next lower number is 92 -> differenced
= 3.So if we reduce our visual representation of the canvas in the latter example from 95px to 92px and add a 3px wide empty space on one side, we would be able to paint on a canvas of 92px * 1.25 = 115px, which is an integer and thus does not lead to a slightly off zoom factor.
Drawbacks of the solution
The difference calculated in the above example is the maximum difference that can occur with this given zoom factor, since the next higher number would be 96, which is again integrally divisible by 4.
Generally speaking:
d max
=denom(z) - 1
So with a zoom factor of 125%, the empty border will never be wider than 3px. Personally I would trade off 3px of my monitor for a crisp presentation.
With more sophisticated zoom levels this might get worse, though. If someone sets a zoom level of 101% for example (that would be 101/100 in fractional), we have a denominator of 100, meaning that in the worst case we would add 99px of empty space in order to have a crisp presentation. I’m not sure if this is still a good tradeoff.
Bottom line
A good compromise might be to define a maximum width of empty space we would accept when aiming for a crisp presentation. If we would have to add more than this maximum, then fall back to the whole width with blurry rendering. Windows has support for only a limited number of zoom levels:
Browsers generally allow any zoom level, but pre-defined values e.g. in Firefox are limited as well:
So perhaps we can agree that we would cover the vast majority of use cases by supporting the above listed zoom levels. That would mean that the maximum supported denominator would be 10, resulting in a maximum addition of empty space of 9px.
I would assume that everybody would trade 9px of their monitor space if the alternative is a blurry font. Still the 9px are not lost, you could resize your terminal accordingly so that other parts around it get more space. However, I am not sure about this (there’s always someone to complain 😉), so we might make this a configurable option.
Let me know what you think.
IMO trading a few pixels of padding for clear font rendering is a perfectly reasonable thing to do.