More on font rendering

Another technical post today, since I’ve been banging away at revamping a problematic piece of VectorStorm’s API.

In VectorStorm, here’s how one would normally create renderable text, assuming that you have a pointer to a vsFont in a variable called ‘font’:

AddFragment(
    font->CreateString_Fragment(
        FontContext_2D,
        "My String",
        30.0,
        Justification_Center,
        vsBox2D::CenteredBox( vsVector2D(100.f, 50.f) ),
        vsColor(1.0f,1.0f,1.0f,1.0f)
    )
);

That’s a pretty massive line of code — big enough that I needed to split it out into a bunch of lines just so that it doesn’t scroll off the side of the webpage. Ordinarily, one says that a single line of code should be no more than something in the range of 76 to 80 characters long. This one (when not broken out across multiple lines like this) is 166 characters long. That’s a lot of typing to do for something as simple as drawing some text!

This interface is as bad as it is because VectorStorm’s font system is very old, and has had a lot of features slowly bolted onto it over time, and its API has been growing and growing as new features have been added without any coherent plan behind the whole system. The problem with simply going in and refactoring this code is that all this information really is necessary:

AddFragment() registers the created vsFragment so that it gets drawn automatically each frame. We need this.

CreateString_Fragment() is long and wordy. The awkward function naming is because we have an older function CreateString(), which returned a vsDisplayList. That function still exists and works (and is occasionally necessary), so we can’t just replace it. Alternately, we might have called CreateStringInDisplayList() or CreateStringInDisplayList_NoClear(), or one of a few other minor variants which take different parameters or have slightly different behaviours.  Cleaning up this mess of different interfaces was a core goal of the refactoring.

FontContext_2D tells the font renderer whether this text is going into a 2D scene (where Y is down) or into a 3D scene (where Y is up); it needs to draw the text the other way up, if it’s going into a 3D scene. Can’t get rid of this parameter — it’s absolutely essential.   [1]

“My string” is the string which is being built into renderable geometry. Can’t get rid of that.

30.0 is how many units tall we want the string to be. (That’s expressed in whatever units are being used in the local coordinate space. Which should normally be pixels, if you don’t want to cause pain for yourself.) Definitely can’t get rid of this.

Justification_Center tells us how the text should be placed relative to the object it’s attached to. In this case, centered. Alternately, we could say Justification_Left or Justification_Right.

vsBox2D::CenteredBox( vsVector2D(100.f, 50.f) ) creates a box, centered on 0,0, which is 100 units wide and 50 units tall. Our created string will be constrained inside that box. The rendering system might need to give us smaller text than we asked for, or introduce line breaks in order to make it fit into that space. This is tremendously useful functionality, particularly when you’re dealing with user-provided strings, or with translated string data. But in practice, this parameter is really long and fiddly — it’s another parameter which we’d like to improve.

vsColor(1.0f,1.0f,1.0f,1.0f) specifies the color in which to draw the text. In practice, specifying pure white like this doesn’t actually accomplish anything (since the font is defined as being white in the first place). And there’s another version of this function which doesn’t require this parameter — CreateString_NoColor_Fragment().

So each bit of rendered text really does need to specify all these parameters for the text rendering system to know how to handle the text. But when writing the code it’s awkward to need to remember in what order they come, and when reading the code it can be non-obvious what they all mean.

What’s more, in many situations we want to create many different pieces of text all with the same style. And it’s awkward to have to keep specifying and re-specifying all those parameters every time we just want to make another bit of text in a single style. So based on these complaints, I’ve implemented a new, simpler API.

Under the new API, the standard way to draw text in VectorStorm now looks like this:

vsFontRenderer fr(font);
fr.SetSize(30.f);
fr.SetJustificationType( Justification_Center );
fr.SetMaxWidthAndHeight( 100.f, 50.f );
AddFragment( fr.Fragment2D("My string") );

To my eyes, this is much easier to read and understand. It doesn’t require you to set any variables you don’t care about, and you don’t have to try to remember the order that all those parameters are supposed to provided in, in a huge single-function API.

The best bit about this approach is that the ‘vsFontRenderer’ object can be stored and reused, to render more text using the same settings at any point later on. If you want to render to a display list instead, you call fr.DisplayList2D("My string");, or you can use fr.DisplayList2D( myList, "My string" ) if you want to provide your own display list.

Finally, this has allowed me to completely delete all those mildly-tweaked variants of the old font drawing code, so now everything goes through a single implementation no matter whether you want colors or not, or a custom transform or not, or etc, instead of having custom implementations for all those different combinations.

In my book, this change was a big, big win.

For those who are interested, the refactored code is up on VectorStorm’s Github repository, in the v2 branch.

1. Note that most people look at me funny when I tell them that VectorStorm points its Y axis in different directions in 2D scenes than in 3D scenes. But the indisputable and inarguable facts are that in any sane 2D environment, Y increases as you go down the screen, while in any sane 3D environment, Y increases as you move up from the ground, so I’m not the crazy one in this conversation. Also, tabs are better than spaces, vim is better than emacs, cats are better than dogs, and I don’t care what anybody says, big-endian is intrinsically better than little-endian. :)