Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manually draw pixel-perfect glyphs for Box Drawing and Block Elements characters #2409

Closed
anatoly-spb opened this issue Sep 10, 2019 · 29 comments · Fixed by #3416
Closed

Manually draw pixel-perfect glyphs for Box Drawing and Block Elements characters #2409

anatoly-spb opened this issue Sep 10, 2019 · 29 comments · Fixed by #3416
Labels
help wanted type/enhancement Features or improvements to existing features
Milestone

Comments

@anatoly-spb
Copy link

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:

#475
#992

@Tyriar
Copy link
Member

Tyriar commented Sep 10, 2019

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

@anatoly-spb
Copy link
Author

@Tyriar how can I share the needed info?

@Tyriar
Copy link
Member

Tyriar commented Sep 10, 2019

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

@anatoly-spb
Copy link
Author

anatoly-spb commented Sep 10, 2019

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/hack-font@3.3.0/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

@egmontkob
Copy link

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.

@jerch
Copy link
Member

jerch commented Sep 12, 2019

@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.

@Tyriar
Copy link
Member

Tyriar commented Sep 12, 2019

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

@Tyriar Tyriar added area/renderer help wanted type/enhancement Features or improvements to existing features and removed needs more info labels Sep 12, 2019
@Tyriar Tyriar changed the title Line Spacing Issue Manually draw pixel-perfect glyphs for characters used for drawing borders Sep 12, 2019
@egmontkob
Copy link

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.

@Tyriar Tyriar changed the title Manually draw pixel-perfect glyphs for characters used for drawing borders Manually draw pixel-perfect glyphs for Box Drawing and Block Elements characters Nov 22, 2019
@jerch
Copy link
Member

jerch commented Nov 22, 2019

@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...

@egmontkob
Copy link

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

@Tyriar
Copy link
Member

Tyriar commented Nov 22, 2019

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

@gnojus
Copy link

gnojus commented Apr 15, 2020

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.

@guregu
Copy link

guregu commented Apr 28, 2020

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.

@Tyriar
Copy link
Member

Tyriar commented Apr 28, 2020

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

private _drawToCache(code: number, bg: number, fg: number): IRasterizedGlyph;
private _drawToCache(chars: string, bg: number, fg: number): IRasterizedGlyph;
private _drawToCache(codeOrChars: number | string, bg: number, fg: number): IRasterizedGlyph {

@guregu
Copy link

guregu commented Apr 28, 2020

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

@Tyriar
Copy link
Member

Tyriar commented Apr 28, 2020

@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 #2613, #2645, microsoft/vscode#95813

@Tyriar
Copy link
Member

Tyriar commented Apr 28, 2020

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.

@jerch
Copy link
Member

jerch commented Apr 28, 2020

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).

@Tyriar
Copy link
Member

Tyriar commented Apr 28, 2020

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? 🤔

@guregu
Copy link

guregu commented Apr 28, 2020

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.

@guregu
Copy link

guregu commented Apr 28, 2020

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?

@Tyriar
Copy link
Member

Tyriar commented Apr 28, 2020

@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?

@guregu
Copy link

guregu commented May 5, 2020

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

Code is rough, but available here: 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).

@Tyriar
Copy link
Member

Tyriar commented May 5, 2020

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.

@Tyriar
Copy link
Member

Tyriar commented Aug 19, 2021

@meganrogge and I just finished this, here's the result:

Canvas (1.1 line height, 1 letter spacing):

image

Webgl (same):

image

The setting is customGlyphs and is enabled by default

@Tyriar Tyriar added this to the 4.14.0 milestone Aug 19, 2021
@jerch
Copy link
Member

jerch commented Aug 19, 2021

@Tyriar Wow, nice work. 👍 So many pretty boxes - must be xmas already, wonder whats inside 🤔

Edit: Any idea for the DOM renderer yet? Just give any box glyph a tiny bigger font-size?

Edit2: Since you are both at it - how about those other underline styles/colors? 😸

@Tyriar
Copy link
Member

Tyriar commented Aug 19, 2021

Any idea for the DOM renderer yet? Just give any box glyph a tiny bigger font-size?

For the DOM renderer I'm not sure we want to add the complexity there of rendering custom glyphs. Especially since I want to make the canvas renderer its own addon which would mean we don't even need the custom glyph code in the core xterm.js bundle.

how about those other underline styles/colors?

I think underline styles and colors run into issues packing those bits in, there's also the issue that they probably can't just be added to the glyphs like this since they're repeating patterns that would get cut off, ie.

image


Also special callout to @guregu whose work this builds upon ❤️

@jerch
Copy link
Member

jerch commented Aug 19, 2021

... they probably can't just be added to the glyphs like this since they're repeating patterns that would get cut off ...

This was my observation as well when playing around with it here: #1145 (comment)

To me it seems underline might need its own render layer with its own progression only loosely coupled to glyph runwidths (like the wave period depends on the cell's global line position not trying to tie it's shape to glyph start). I learned that lesson when debugging browsers standard behavior, which tries to tie the dashed/dotted shape perfectly on glyph start and width, but they kinda fail at various font size miserably (see images in other comment above).

@Tyriar
Copy link
Member

Tyriar commented Aug 19, 2021

That's what the link render layer is which is actually also used in webgl for convenience, it just covers hover link underlines right now.

It could be done in it's own webgl render pass as well, using an underline style texture atlas and coordinates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted type/enhancement Features or improvements to existing features
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants