Early in 2025, I had stumbled on concept images of a font with triangular glyphs. Something about it was appealing. I ended up crafting a similar concept, but extending it so that the equilateral triangles could tile together using both upward and downward facing triangles:

The upward and downward facing glyphs for the triangle font, from A to Z.
I was implementing OpenGL font rendering in C++, using Sean Barrett’s TrueType library to load fonts and construct a font atlas. Somewhere in this whole process I decided it would be fun to implement a font I had conceptualized months ago for real, so to speak, as a TrueType font.
TrueType doesn’t support conditional logic for alternating glyph directions like this, so in practice I achieve the effect by making all uppercase characters be upward facing triangles and all lowercase characters be downward facing triangles. The text below is “HeLlO wOrLd”:
Having my own game that could load and use the font was helpful for debugging, once the binary was loadable, since I could step through it with a debugger. I also found fontdrop and the hex editor useful.
You can download the font here.
Architecture
There are two fundamental responsibilites: defining the font in memory and serializing it to .ttf:
FontDefinition font = CreateFont();
bool success = ExportFont(&font);
Separating font definition from binary export keeps the system reusable and avoids hard-coded constants leaking into the writer.
CreateFont populates a builder struct:
FontDefinition font;
assert(InitFontDefinition(&font,
/*units_per_em=*/1000, /*ascent=*/866,
/*descent=*/0, /*line_gap=*/0,
/*family_name=*/"TeSsElLaTe",
/*subfamily_name=*/"Regular",
/*unique_name=*/"TeSsElLaTe rEgUlAr v1.0",
/*full_name=*/"TeSsElLaTe rEgUlAr")
&& "Failed to initialize font");
// Create the missing glyph (.notdef) - a simple square
// This glyph is shown when a character is not found in the font
const GlyphId missing_glyph = StartGlyph(&font, /*codepoint=*/0, advance_width, left_side_bearing);
assert(missing_glyph != 0xFFFF && "Failed to start missing glyph");
// Create a square contour with 4 points
assert(StartContour(&font) && "Failed to start contour for missing glyph");
assert(AddPoint(&font, 0, 0, 1) && "Failed to add point 0");
assert(AddPoint(&font, 1000, 0, 1) && "Failed to add point 1");
assert(AddPoint(&font, 1000, 1000, 1) && "Failed to add point 2");
assert(AddPoint(&font, 0, 1000, 1) && "Failed to add point 3");
assert(EndContour(&font) && "Failed to end contour for missing glyph");
assert(EndGlyph(&font) && "Failed to end missing glyph");
The font definition is kept in a truetype.hpp header, along with builder helpers like StartContour and AddPoint. After defining all of the glyphs, we can also add kerning pairs. The default xadvance is set for subsequent equilateral triangles, and I reduce that for pairs that nest together.
ExportFont needs to open a file and write the binary .ttf file. The format consists of a directory listing the tables and their offsets, followed by the tables themselves, padded to 4-bytes. Each table has a checksum, and the data is exported as big-endian (not the default when using fwrite).
I wanted to calculate the table checksums as I wrote to disk to avoid multiple passes over the data. This requirement was realized via a TableWriter struct along with some basic helper methods like WriteU16BE(&writer, value), which exports the value in big-endian and updates the checksum.
struct TableWriter {
FILE* file;
u32 checksum;
u8 word_buffer[4];
u32 word_buffer_index; // 0-4
};
All WriteXXBE helper methods call down to a void FeedBytesToChecksum(TableWriter* writer, const u8* data, u32 size); method, which appropriately updates the checksum and writes the data to the file.
After writing placeholder values for the directory, the exporter writes all of the tables, in alphabetical order. Each table is written as follows:
// Write cmap table
writer.checksum = 0; // Reset.
TableInfo* table_info = &table_infos[table_index];
table_info->offset = (u32)ftell(writer.file);
table_info->length = WriteCmapTable(&writer, font);
table_info->checksum = writer.checksum;
PadTo4ByteAlignment(&writer);
printf("Wrote %s table (%u bytes, offset %u, checksum 0x%08X)\n",
table_tags[table_index], table_info->length, table_info->offset, table_info->checksum);
table_index++;
Offset values are captured directly from the file stream as each table is written to disk, enabling a single streaming pass without rewinding or recomputing. Each table has its own WriteXXXXTable method, all of which were kept in a truetype_export.hpp header. This made it easy to iterate on the code.
We then go back and update the table directory. Easy peasy.
Finishing a Vertical Slice
I didn’t know whether the font exporter was working until I was able to load the resulting .ttf file in another program. A vertical slice lets you de-risk binary exporters early. I prioritized this by not immediately defining all glyphs and instead just defined the undefined glyph (.notdef), space, and ‘A’. I implemented the minimum necessary set of TrueType tables that could be loaded by tools like stb_truetype and the browser inspector, helping me confirm correctness before scaling out to A–Z and a-z.
Having a testable vertical slice is essential when coding on any project, whether solo or on a team. Coding without knowing whether your code is working is the same as flying blind. A hex editor provides directional confidence that the structure is forming correctly, but real validation only comes when another program successfully loads the font.
Bugs
Sometimes it is interesting to look at what sorts of issues you run into. Understanding how a process fails tells you where to focus on improvements. The bugs that I spent time investigating were:
- Glyph alignment (segfault). Each glyph must be word-aligned. IMO, this is not at all obvious from the glyf table documentation.
- Malformed contours as a result of exporting glyph vertices rather than relative vertices. This was clearly laid out in the documentation.

Image from developer.apple.com.
That was enough to get it working with stb_truetype and my font rendering. In order to get the chrome console to be happy, I additionally had to:
- Fix the
cmaplength, which was miscalculated. Code like this is not ideal:
// Calculate subtable length
// Format 4 header: 14 bytes (format, length, language, segCountX2, searchRange, entrySelector, rangeShift)
// Data: reservedPad (2) + 4 arrays of seg_count u16 values (seg_count * 8)
const u16 subtable_length = 14 + 2 + (seg_count * 8);
- Needing to additionally export the OS/2 table, which includes metrics needed by Windows. The chrome inspector straight up told me to add this.
- Table alignment (browser load failure). All tables must be 4-byte aligned. This was in the top-level docs:

Image from apple.developer.com.
- Lastly, kerning was working in my font rendering but not for all characters in chrome. It turned out that almost all of my glyphs start at (0,0), but a few, like ‘d’, did not. Chrome was rendering these further to the left. I had to set the left side bearing for those glyphs.
So two counts of failing to use alignment. Seems like a trend I can be more aware of. Despite chasing binary alignment and metrics, I ran into no memory safety issues like null pointers or leaks — problems typically cited as risks of low-level code.
Initializer Lists
I am trying to write in a Muratori-inspired minimal C++ style. That is, C++ without a lot of the C++ features. Avoid classes, macros, templates, and the standard libraries. Why? Ostensibly because simpler code is sufficient, and then easier to understand and faster to compile / execute. Though doing it because I think it is interesting and I admire people like Casey and attitudes like this one from Chris Wellons is also a perfectly valid reason.
I was able to author everything to adhere to this style, but found that I really did want to use initializer lists to simplify glyph creation:
assert(AddGlyph(&font, 'A', advance_width, left_side_bearing,
{{A, D, N, O, E, B, C}, {S, L, R}}) && "Failed to add A glyph");
I totally could do that without it:
const GlyphId glyph = StartGlyph(&font, 'A', advance_width, left_side_bearing);
assert(glyph != 0xFFFF && "Failed to start glyph");
assert(StartContour(&font) && "Failed to start contour");
assert(AddPoint(&font, font.units_per_em*A.x, font.units_per_em*A.y, 1) && "Failed to add point");
assert(AddPoint(&font, font.units_per_em*D.x, font.units_per_em*D.y, 1) && "Failed to add point");
assert(AddPoint(&font, font.units_per_em*N.x, font.units_per_em*N.y, 1) && "Failed to add point");
assert(AddPoint(&font, font.units_per_em*O.x, font.units_per_em*O.y, 1) && "Failed to add point");
assert(AddPoint(&font, font.units_per_em*E.x, font.units_per_em*E.y, 1) && "Failed to add point");
assert(AddPoint(&font, font.units_per_em*B.x, font.units_per_em*B.y, 1) && "Failed to add point");
assert(AddPoint(&font, font.units_per_em*C.x, font.units_per_em*C.y, 1) && "Failed to add point");
assert(EndContour(&font) && "Failed to end contour");
assert(StartContour(&font) && "Failed to start contour");
assert(AddPoint(&font, font.units_per_em*S.x, font.units_per_em*S.y, 1) && "Failed to add point");
assert(AddPoint(&font, font.units_per_em*L.x, font.units_per_em*L.y, 1) && "Failed to add point");
assert(AddPoint(&font, font.units_per_em*R.x, font.units_per_em*R.y, 1) && "Failed to add point");
assert(EndContour(&font) && "Failed to end contour");
assert(EndGlyph(&font) && "Failed to end glyph");
You can see why I wanted to save myself the typing.
Unfortunately, in addition to including <initializer_list>, I also ended up including <vector> because I was calling AddGlyph with a transform for all downward facing glyphs:
assert(AddGlyph(&font, 'f', advance_width, left_side_bearing,
ReflectToDownwardGlyph({{A,B,F,J,N,U,V,S,G,C}}, t))
&& "Failed to add f glyph");
Unfortunately, I couldn’t have ReflectToDownwardGlyph modify an initializer list and produce a new one. Instead, I had to return an std::vector. Oh well.
There probably are reasonable ways to do this in an minimal style. If you happen to know, please send me a message!
Conclusion
It was fun to work on a self-contained, somewhat artistic project. I got a chance to try both the Cursor and Antigravity agentic IDEs, both of which worked quite well.
I’m not sure that I’ll be able to author posts monthly, but hopefully this is a good start to returning to my creative outlet. Happy Holidays!