eieio.games

by nolen royalty

Hexagone

A font that converts hex color codes to RGB

Aug 26, 2023

So here’s a neat trick:

the font is called hexagone because it gets rid of hex

By changing the font I’m able to automatically convert hex color codes to RGB! What’s going on?

A quick demo

That video might be a little hard to follow - let’s start with a demo so that you can try Hexagone yourself. Type in any hex color code followed by an equals sign (like #ecd17a=) - it’ll automatically be converted to RGB. There’s no javascript here, just font trickery.

Maybe unsurprisingly this demo is sometimes buggy! Hopefully it worked for you. Let’s get into what’s going on.

Fonts are powerful - why?

Hexagone takes advantage of two powerful features of fonts: ligatures and contextual alternates.

We use ligatures to combine multiple glyphs into one. For example, some fonts use ligatures to turn “tt” into a single character with an unbroken horizontal bar that goes through both characters.

We use contextual alternates to swap one glyph out for another one based on the surrounding characters. For example, when you type a fraction like “1/2” some fonts will notice and replace the “1” with a superscript 1, the “2” with a subscript “2”, and the ”/” with a more fractiony-looking glyph. You could imagine doing that specific substitution with a ligature (swapping out “1/2” for a single “1/2” glyph) but contextual alternates allow you to more easily generalize that logic across a wide range of fractions.

And those are just some simple and very English-centric examples! I don’t understand the details very well, but I believe that Arabic has whole characters that are swapped out using contextual alternates!

So fonts are powerful because language is powerful and the way that we write language is complicated. But what primitives are actually available?

Lookups on lookups

Hexagone works by chaining a series of replacement operations together. OpenType offers many alternatives for how to write your replacement operations but ultimately they all come down to “look for a pattern and replace part of that pattern with a different pattern.” Here are some examples

# If a character is followed by a ', that character is targeted
# for replacement and any characters *without* a ' suffix are
# just for pattern matching.

# Replace all As with Bs
sub a by B;

# Replace all lower case letters with upper-case letters
sub [a - z] by [A - Z];

# Replace two ts with a single "double-t" glyph
sub t t by t_t;

# Replace "A" with "B" - only if the A has a "P" on both sides:
sub P A' P by B;

# Replace f f according to the rules in a table named "HEX_DEC"
sub f' lookup HEX_DEC f';

# Replace "f f" with 255 - only if it's preceded by a #
# and followed by an equals
sub numbersign f' f' equals by two five five;

Maybe those last examples give you a sense of how hexagone works - we look for things that look like hex and are surrounded by our activation characters and we swap them out.

Except that I lied to you. That last rule doesn’t work. Font shaping has some more restrictions!

Fonts have restrictions (but it’d be more fun if they didn’t)

There are two major restrictions I ran into while making hexagone:

  1. Directly substituting multiple glyphs for multiple glyphs isn’t allowed.
  2. The internal replacement “cursor” always moves forward.

1. is probably pretty clear - it’s what prevents that final rule in the last section from working (the error you get is “Direct substitution of multiple glyphs by multiple glyphs is not supported.” ).

2. Might be a little harder to picture. The basic idea is: let’s say that you have the text “ABC” and you’ve just written a rule to replace “B” with “DD” - so now your text is “ADDC.” If you have a rule that looks for “AD” and replaces it with “E” the rule won’t fire - your cursor has already passed that “A” and so you can’t go back and run a replacement on it now 1!

1

I suspect I’m not quite describing this right but it’s my rough mental model of how a cursor within a specific OpenType feature works. Shout at me if I’m wrong and I’ll update the page!

I believe rule 1 exists to ensure that we don’t lose information when converting between fonts, and that rule 2 exists because without it you could easily make a non-halting font (I would love this! but it is probably not a good idea).

Workarounds and a basic algorithm

However! We can work around both of these problems with a few tricks.

  • We can replace multiple glyphs with a single glyph, or replace a single glyph with multiple glyphs.
  • The ligature and contextual alternate bits of font-shaping run on separate passes

And this gives us enough power to create hexagone. The rough logic is:

  • Make up a single glyph for every pair of two hex digits (from zero_zero to f_f) and stuff them into the font somewhere.
  • On the ligature pass, identify groups of hex digits that we want to replace with rgb and replace them with the glyphs that we’ve made up. For example, replace “ef” with the glyph “e_f.”
  • On the contextual alternates pass, replace our made-up glyphs with their decimal equivalents. For example, replace “1_a” with “26,” or 26).

This ends up looking something like this:

# Character class defining all the glyphs we've made up
@mine=[zero_zero zero_one ... f_f];
# Same as @mine but with .end appended
@mineend=[zero_zero.end ... f_f.end];
# Character class that identifies potential hex
@hex=[zero one two ... f]

# Map hex to our glyphs
lookup LIGA_NOTLAST {
   sub zero zero by zero_zero;
   sub zero one by zero_one;
   ...
   sub f e by f_e;
   sub f f by f_f;
} LIGA_NOTLAST;

# The same as liga_notlast but using ".end" glyphs
lookup LIGA_LAST {
    sub zero zero by zero_zero.end
    ...
} LIGA_LAST;

# Map our glyphs back to decimal
lookup HEX_DEC {
   sub zero_zero' by zero comma;
   sub zero_one' by one comma;
   ...
   sub f_e' by two five four comma;
   sub f_f' by two five five comma;
}

# The same as hex_dec but we use a paren instead of a comma.
lookup HEX_DEC_LAST {
   sub zero_zero.end' by zero rightparen;
   ...
} HEX_DEC_LAST;

feature liga {
    sub numbersign @hex' lookup LIGA_NOTLAST @hex' @hex @hex @hex @hex equal;
    sub numbersign @mine @hex' lookup LIGA_NOTLAST @hex'  @hex @hex equal;
    sub numbersign @mine @mine @hex' lookup LIGA_LAST @hex' equal';
} liga;

feature calt {
    sub numbersign' @mine @mine @mineend by r g b colon parenleft;
    sub @mine' lookup HEX_DEC;
    sub @mineend' lookup HEX_DEC_END;
} calt;

And that’s about it! I generated this file via a script and used python fontforge bindings to add the shaping logic to an existing font. You can see the code I wrote for this here. Be warned: I wrote it fast and it’s pretty bad!

Why’d you build this? Is it useful?

Well, I don’t think it’s super useful!

I was introduced to the power of fonts back when I saw Tristan Hume’s numderline and when I started at the Recurse Center I thought it might be fun to build a game inside a font.

Recurse recently ran an “impossible stuff day” (roughly a day where you take on an ambitious project that you’re not sure you can actually do) and I decided to spend the day trying to build Hexagone to figure out what a “game in a font” might look like. I picked Hexagone because it seemed challenging and interesting but also tractable, and because I came up with the name and thought it was funny (this was the primary reason).

Building Hexagone ended up being far from impossible, but it didn’t make me particularly excited about going further with a game. But I learned a lot and had a lot of fun!

Maybe Hexagone will inspire you to build your own nonsense font.

Acknowledgements

As I said above, I would never have come up with this idea without seeing numderline - thanks Tristan!

I built Hexagone at the Recurse Center, a place that is like a writers retreat but for programming. It’s a lovely place to improve as a programmer and also a super supportive environment for odd nonsense like this (if that sounds fun to you you should apply!).

I also learned a lot about font stuff from this Litherum post about an addition font, this info about the sparks font, and this post from Matthew Skala. Thanks all!

Thanks for reading!

Keep up with me on my socials 👆

Or sub to my newsletter here! 👇