Xterm.js: Manually draw pixel-perfect glyphs for Box Drawing and Block Elements characters

Created on 10 Sep 2019  ยท  24Comments  ยท  Source: xtermjs/xterm.js


Details

  • Browser and browser version: Chrome 76.0.3809.132 (64 bit), FireFox 69.0 (64 bit)
  • OS version: Windows 7 Pro 64 bit
  • xterm.js version: ^3.14.5

Steps to reproduce

index.html:

<!doctype html>
<html>

<head>
    <link rel="stylesheet" href="node_modules/xterm/dist/xterm.css" />
    <script src="node_modules/xterm/dist/xterm.js"></script>
</head>

<body>
    <div id="terminal"></div>
    <script>
        var options = {
            rows: 30,
            cols: 80,
            fontFamily: '"Courier New", "DejaVu Sans Mono", "Everson Mono", FreeMono, "Andale Mono", monospace',
            fontSize: 12,
            convertEol: true
        };
        var term = new Terminal(options);
        term.open(document.getElementById('terminal'));

        term.write('           โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ••\n');
        term.write('           โ•‘                                                         โ”‚\n');
        term.write('           โ•‘              โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฆโ•โ•โ•โ•โ•โ•โ•โ•โ•คโ•โ•โ•โ•โ•โ•โ•โ•โ•—        โ”‚\n');
        term.write('           โ•‘              โ•‘               โ•‘        โ”‚        โ•‘        โ”‚\n');
        term.write('           โ•‘              โ•‘               โ•‘        โ”‚        โ•‘        โ”‚\n');
    </script>
</body>

</html>

image

There are several related issues but it does not help:

https://github.com/xtermjs/xterm.js/issues/475
https://github.com/xtermjs/xterm.js/issues/992

arerenderer help wanted typenhancement

Most helpful comment

I've done some experimenting with manually drawing certain characters.
Demo page: https://roguelike.solutions/xterm/xtermtest.html

Code is rough, but available here: https://github.com/xtermjs/xterm.js/compare/master...guregu:box-drawing

Currently, it's two parts, one is boxDrawingLineSegments which borrows an algorithm from iTerm2. It defines 7 kind of arbitrary vertical and horizontal anchor points per character cell and strokes lines or curves corresponding to them. This works for almost all of the box-drawing lines, but doesn't work for the dotted line characters which require more anchor points. Having precise control over drawing the lines also means we don't need to clip horizontally to avoid spillover. I'm trying to think of a better way to represent these that will also work with the dotted lines.
Perhaps instead of splitting cells into 7 somewhat-arbitrary points, instead we can define the locations like "left", "center", "center minus n devicePixelRatio pixels", etc.
Also, iTerm2 is GPL2 and I tried to write it to be as original as possible and avoid license issues, but I'm not sure where the figurative line is drawn here. This is another reason to use a fancier original algorithm. Consider the current implementation a proof of concept.

Next is boxDrawingBoxes (needs a better name ๐Ÿ˜…) which splits a character cell into eighths and fills in rectangles corresponding to them. This is sufficient to draw the solid block characters, including new ones from the Symbols for Legacy Computing block. This should be enough to support the quadrant characters too, but in order to support sextants it will need to split characters into sixths. Allowing a divisor to be defined would be enough to support most block-like characters.

I don't yet support the polygon characters from the Symbols for Legacy Computing block, but using similar techniques should work for them. I'm not sure how to go about drawing the shade characters like โ–‘ โ–’ โ–“, but maybe they could be special-cased. There's also the question of how far we want to go with this.

To clean the code up, maybe we could define an interface for drawing segments, and its implementors could be lines, curves, n-th boxes, etc. This is all very experimental, but if it seems like a good idea I'd be happy to contribute (and add support for the WebGL renderer).

All 24 comments

I think this depends on the font being used, I can't repro using the canvas (default) renderer.

@Tyriar how can I share the needed info?

Can you try using this font? Both your fontFamily and Hack work for me on my Windows 10 box.

I have tried the following on my home Windows 10 64 bit machine in Yandex Browser (based on Chromium):

<!doctype html>
<html>

<head>
    <link rel='stylesheet' href='//cdn.jsdelivr.net/npm/[email protected]/build/web/hack.css'>
    <link rel="stylesheet" href="node_modules/xterm/dist/xterm.css" />
    <script src="node_modules/xterm/dist/xterm.js"></script>
</head>

<body>
    <div id="terminal"></div>
    <script>
        var options = {
            rows: 30,
            cols: 80,
            fontFamily: 'Hack',
            fontSize: 12,
            convertEol: true
        };
        var term = new Terminal(options);
        term.open(document.getElementById('terminal'));

        term.write('           โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ••\n');
        term.write('           โ•‘                                                         โ”‚\n');
        term.write('           โ•‘              โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฆโ•โ•โ•โ•โ•โ•โ•โ•โ•คโ•โ•โ•โ•โ•โ•โ•โ•โ•—        โ”‚\n');
        term.write('           โ•‘              โ•‘               โ•‘        โ”‚        โ•‘        โ”‚\n');
        term.write('           โ•‘              โ•‘               โ•‘        โ”‚        โ•‘        โ”‚\n');
    </script>
</body>

</html>

Here is the result:
image

The same on Chrome:
image

Also I tried to print in Visual Studio Code with node:
image

FYI: Some terminals, including gnome-terminal (vte) and konsole manually draw the U+2500..257F (or even up to 259F) characters, rather than taking them from the font, in order to provide beautifully connected look.

Coincidentally both of these emulators took pretty similar technical approach: define the look of (most of) these glyphs as a 5x5 bitmap with uneven "pixel" sizes that are computed runtime based on font size.

@egmontkob Lulz, I remember doing it likewise in my old emulator. Always wondered why xterm.js does not show that behavior, but never investigated.

@Tyriar Just did a few tests with different fonts with chrome and FF with these results - I see in every engine with every font listed in the demo a tiny space (tested in canvas and DOM renderer). The space itself varies with font size and the font itself, FF is one pixel off with a bigger gap (we have that pixel offset in FF also at other places/issues).

Conclusion: All of my tested fonts show spaces (from hardly visible to a prominent gap), therefore I think that the glyphs simply dont cover the height completely (just a guess, have not inspected single glyphs with a font tool). The differences in spaces are mostly font renderer related that tries to round/align it to some given font size.

Furthermore I dont see any gap in vscode (not even the slightest, tested with mc). Not sure what vscode does differently here, maybe a general fix can borrow that? Beside drawing own glyphs as suggested by @egmontkob we might get away with a simpler approach for DOM - just draw those codepoints in question at slightly bigger font size. For the other renderers where we maintain canvas glyph states anyway @egmontkob's approach might get better results.

Perhaps vscode (or at least my setup) is just lucky with its font size, family, window zoom level, etc.

we might get away with a simpler approach for DOM - just draw those codepoints in question at slightly bigger font size

We don't and should not imo clip the cells in the DOM renderer so I don't think this is an option.

Provided it doesn't add too much code* we could hardcode our own glyphs for these ๐Ÿ‘, it would be good if it was consistent with the DOM renderer though.

* I have doubts on this

Now that #2572 has been marked as dup, this bug is definitely about U+2500..257F "Box Drawing" and U+2580..259F "Block Elements" too.

Also keep an eye on forthcoming (Unicode 13) U+1FB00 "Symbols for Legacy Computing", see e.g. VTE 189.

@egmontkob Lol, I guess those newish glyphs are meant to align perfectly as well? Geez, who needs a font renderer, if one can do it the hard way...

I'd love to understand the reasons none of the fonts gets it right. But I don't have time and motivation to investigate.

One interesting thing this could do that fonts couldn't is fill the entire cell when a line height is specified.

Additional funny behavior (vscode terminal):
out3
For me, changing font sizes, using the previously mentioned hack font does not help, the spacing remains. However, judging from the gif above, the block characters seems to be placed incorerctly. There is one pixel gap at the bottom and the difference between 7/8 and 8/8 (full) blocks seems smaller than between the other ones.

On macOS (Retina screen, devicePixelRatio=2) + Chrome, using the Hack font gives me small vertical gaps like this:
Screen Shot 2020-04-28 at 20 44 51
But after poking around I noticed that turning BaseRenderLayer._clipRow into a no-op renders them correctly:
Screen Shot 2020-04-28 at 20 44 40
which leads me to believe that this might be a clipping issue.

@guregu the canvas renderer by design must clip the rows or it will end up having artifacts showing up, but anyway the plan is to remove the canvas renderer in favor of DOM and webgl (basically superior in every way to canvas).

This is still marked as help wanted and maybe it can/should only be done in the webgl renderer in which case it would be a matter of handling those particular characters specially when drawing the char to the texture atlas:

https://github.com/xtermjs/xterm.js/blob/45077d710627fb2aec61ac2dfc41879d8c32371e/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts#L304-L306

FWIW I've managed to fix it without messing with the clipping.
The problem wasn't the clipping, but using middle as the textBaseline caused it to be slightly misaligned.
Changing the textBaseline to bottom totally fixed the issue for me.
Diff: https://github.com/xtermjs/xterm.js/compare/master...guregu:master
I can send a PR if it looks good, but I'm not sure if this will screw something else up.

Is there a particular reason it was set to middle before?
I noticed that it sets normal text ever so slightly lower, but it looks equivalent to how Terminal.app renders text.

Here's a screenshot of the code running from the start of this PR without the fix:
Screen Shot 2020-04-28 at 22 29 03
Here it is with the fix applied:
Screen Shot 2020-04-28 at 22 24 19

@guregu previously the webgl renderer used top, but it ended up making the text top aligned:

image

I fear this will happen if we use bottom

See https://github.com/xtermjs/xterm.js/issues/2613, https://github.com/xtermjs/xterm.js/issues/2645, https://github.com/microsoft/vscode/issues/95813

It's also not perfect even ignoring the bottom alignment problem (notice the notches on the horizontal line):

image

I think manually drawing perfect glyphs is the way to go.

Btw @nojus297 output pretty much looks like an 0.5 px offset issue. I stumbled over antialiasing issues with a 0.5 offset myself the other day, when I messed around with self-drawn curly underlines. Not sure if this applies here at all (still not used to the renderer code lol).

Would could check your zoom level as well to make sure it's 1. Should we maybe be bumping it down 0.5px when the font size is odd? ๐Ÿค”

I took screenshots of both my changes and the unchanged version and ran a diff on them. Normal text actually renders exactly the same. The only difference is the box drawing characters.
download
(the magenta is the differing part)

Basically the crux of this issue is that, for some reason, text that takes up the entire height of a cell gets pushed up a few pixels and clipped out when using middle as the textBaseline.
It's easy to verify this if you highlight it.
XTERM_BROKE
^ unfixed version, you can see that there's a few empty pixels at the bottom of the โ•‘

Regarding the notches on the horizontal line, Terminal.app appears to have the same problem. Maybe it's a font issue? No idea.
Screenshot from Terminal.app:
Screen Shot 2020-04-29 at 0 02 04

However, even with the changes I still see some weirdness, like using the cursor causes tiny black notches to show up. This doesn't happen on the veritcal ones. Maybe related to the other notches?
This is from xterm.js, and happens on both the changed and unchanged versions:
rendering-oddness

Anyway, I agree rendering these characters specially is the way to go. Just trying to find the reason this is happening in the first place.

I had some friends test out my fix on Windows and it actually makes things worse ๐Ÿ˜…
Looks like special casing it is the only surefire way. Sorry about that.
The cursor thing is probably a separate issue. Clearing the whole canvas during clearCursor fixes it. Seems like the horizontal box-drawing character is drawing a bit wider than the cell width?

@guregu thanks for looking into it.

Seems like the horizontal box-drawing character is drawing a bit wider than the cell width?

Maybe, we allow that. Something that may work is to clip the character glyph to the size of a cell only for these box drawing characters?

I've done some experimenting with manually drawing certain characters.
Demo page: https://roguelike.solutions/xterm/xtermtest.html

Code is rough, but available here: https://github.com/xtermjs/xterm.js/compare/master...guregu:box-drawing

Currently, it's two parts, one is boxDrawingLineSegments which borrows an algorithm from iTerm2. It defines 7 kind of arbitrary vertical and horizontal anchor points per character cell and strokes lines or curves corresponding to them. This works for almost all of the box-drawing lines, but doesn't work for the dotted line characters which require more anchor points. Having precise control over drawing the lines also means we don't need to clip horizontally to avoid spillover. I'm trying to think of a better way to represent these that will also work with the dotted lines.
Perhaps instead of splitting cells into 7 somewhat-arbitrary points, instead we can define the locations like "left", "center", "center minus n devicePixelRatio pixels", etc.
Also, iTerm2 is GPL2 and I tried to write it to be as original as possible and avoid license issues, but I'm not sure where the figurative line is drawn here. This is another reason to use a fancier original algorithm. Consider the current implementation a proof of concept.

Next is boxDrawingBoxes (needs a better name ๐Ÿ˜…) which splits a character cell into eighths and fills in rectangles corresponding to them. This is sufficient to draw the solid block characters, including new ones from the Symbols for Legacy Computing block. This should be enough to support the quadrant characters too, but in order to support sextants it will need to split characters into sixths. Allowing a divisor to be defined would be enough to support most block-like characters.

I don't yet support the polygon characters from the Symbols for Legacy Computing block, but using similar techniques should work for them. I'm not sure how to go about drawing the shade characters like โ–‘ โ–’ โ–“, but maybe they could be special-cased. There's also the question of how far we want to go with this.

To clean the code up, maybe we could define an interface for drawing segments, and its implementors could be lines, curves, n-th boxes, etc. This is all very experimental, but if it seems like a good idea I'd be happy to contribute (and add support for the WebGL renderer).

This is all very experimental, but if it seems like a good idea I'd be happy to contribute (and add support for the WebGL renderer).

@guregu nice work, a contribution would be awesome, you just need to make sure it's not too derivative of the iTerm2 algorithm as GPL2 is sticky. I'd suggest only doing this for the webgl renderer (not canvas as it's going to be scrapped). Definitely based the lines on devicePixelRatios so the width is great on all monitors.

but using similar techniques should work for them. I'm not sure how to go about drawing the shade characters like โ–‘ โ–’ โ–“

These should be relatively easy to do but this could easily be deferred as well. The main problem in this area are the solid ones.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

johnpoth picture johnpoth  ยท  3Comments

chris-tse picture chris-tse  ยท  4Comments

jerch picture jerch  ยท  3Comments

zhangjie2012 picture zhangjie2012  ยท  3Comments

circuitry2 picture circuitry2  ยท  4Comments