question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Renderer is blurry when window zoom level is changed

See original GitHub issue

https://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:closed
  • Created 4 years ago
  • Reactions:13
  • Comments:16 (6 by maintainers)

github_iconTop GitHub Comments

7reactions
carlfriedrichcommented, May 10, 2022

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

  1. The issue appears only when zooming is applied to the terminal.
  2. It does not matter where the zooming comes from (can be the Windows DPI setting or the browser/parent window, e.g. VS Code).
  3. Zooming is a necessary, but not a sufficient condition, i.e. there are cases where the rendering is crisp despite of an applied zoom factor.

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:

  • Detect whether a zoom is applied to the page.
  • If no:
    • Just draw everything on the canvas normally.
  • If yes:
    • Increase the canvas size by the zoom factor.
    • Draw everything on the canvas in the zoomed size.
    • Reduce the canvas to its original (non-zoomed) size.
    • The browser then increases the canvas to the zoomed size again, as it applies the zoom factor to the complete webpage.

Let’s use the following symbols to describe the behaviour mathematically:

w: canvas width z: zoom factor

The 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 size

w' / 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 and w', which makes us eventually stumble upon rounding errors.

Let’s see two examples:

  1. No rounding error

    Let’s assume a scaling factor of 125% and a canvas width of 100px:

    w' = w * z w' = 100 * 1.25 w' = 125

    So the terminal is drawn to a 125px wide canvas. Afterwards it is zoomed down:

    w' / z = w 125 / 1.25 = 100

    So 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 = 125

    So in the end the canvas is displayed in the same size it was drawn to, everyhing looks crisp.

  2. 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.25 w' = 118.75

    Since the canvas has to have a discrete size, we have to round it:

    ⌊w'⌉ = 119

    In the HTML document, we’re styling the canvas with it original width of 95, so:

    w = 95

    The 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.25 w' = 118.75

    Result: 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:

<canvas width="119" height="119" style="width: 95px; height: 95px;"></canvas>

(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 / 95 z' ≈ 1,2526315789 ≠ 1.25

It 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 width w' 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 until w * 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.

  1. w = 100px 100 is integrally divisible by 4. No adjustments necessary.
  2. w = 95px 95 is not integrally divisible by 4. Next lower number is 92 -> difference d = 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:

125% (=5/4), 150% (=3/2), 175% (=7/4), 200% (=2/1)

Browsers generally allow any zoom level, but pre-defined values e.g. in Firefox are limited as well:

30% (=3/10), 50% (=1/2), 67% (=2/3), 80% (=4/5), 90% (=9/10), 110% (=11/10), 125% (=5/4), ...

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.

4reactions
Eugenycommented, Feb 4, 2022

IMO trading a few pixels of padding for clear font rendering is a perfectly reasonable thing to do.

Read more comments on GitHub >

github_iconTop 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 >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found