flak rss random

experiment with texture healing (monospace kerning)

The monaspace fonts introduced the idea of texture healing. I think of it as a sort of kerning for monospace fonts, though there’s probably some reason that’s technically incorrect. The basic idea is that some letters want more space, while others want less space, but this is hard to achieve in a monospace grid. And so they made a special font that includes alternative glyphs for letter pairs, so that i can donate some space to m.

This is cool, but it requires using a special font. Or a fair bit of work to redesign your own chosen font. Also, there’s a few artifacts, where the adjacent m in immediate are different widths, which can be distracting. But maybe there’s another way to implement this.

If you happen to have written a terminal emulator using OpenGL for rendering, it’s a pretty simple matter to shift the vertices for each cell over by a tiny amount, making some a bit larger and some a bit smaller. We don’t want to do this by a fixed amount, though, or we’ve just poorly reimplmented proportional fonts.

What I did instead is calculate the total “weight” of each word separately. Each letter is 100, except i is 80 and m is 120. Divide by word length to get an average, and now we have a scaling factor for each cell in this word. But the entire word will always be grid aligned, and every letter in the word will look the same.

maybe something like this
var scales [240]float32
var wordweight float32
var wordstart int
var inword bool
for i := range row {
        c := row[i].char
        if c >= 'a' && c <= 'z' {
                if !inword {
                        inword = true
                        wordstart = i
                if c == 'i' || c == 'l' || c == 'j' {
                        wordweight += 80
                } else if c == 'm' || c == 'w' {
                        wordweight += 120
                } else {
                        wordweight += 100
        } else {
                if inword {
                        inword = false
                        avg := wordweight / float32(i-wordstart)
                        for x := wordstart; x < i; x++ {
                                scales[x] = avg
                wordweight = 0
                scales[i] = 100.0

A little later in the render loop, we set up the vertices by shifting the x value. I happen to have an unused z coordinate, and wanted to bang this out quickly, so that’s where the offset went.

if c == 'i' || c == 'l' || c == 'j' {
        w = 80
} else if c == 'm' || c == 'w' {
        w = 120
offset = (w/scales[i] - 1.0) * 0.01

for {
        v.xy[2] = runningoffset
        switch k {
        case 2:
                v.xy[2] += offset
        case 3:
                v.xy[2] += offset
        case 5:
                v.xy[2] += offset
runningoffset += offset

And then just one line in the vertex shader.

xy.x += coord2d.z;

The results are pretty good after an hour of tinkering. This is with the Ubuntu Mono font.

The words swimming and spilling look a little healthier. If you look very closely, you can also see that the n and g are ever so slightly out of position. A long string of wide and narrow characters will make the grid deviation more obvious.

Without adjustment, for comparison.

Posted 11 Nov 2023 22:02 by tedu Updated: 11 Nov 2023 22:02
Tagged: software