This article is the first of a series on text rendering. It introduces the text engine used for the chronotext experiments since 2016. The code of the text engine can be found here.

Assuming that you have read Moving on paths

Here is a WebGL example showing what’s taking place in Sketch.h and Sketch.cpp from the Drawing2dText project’s source folder. Follow the instructions in the code repository to build and run the example on your computer.

Sketch.h

chr::xf::FontManager fontManager;
std::shared_ptr<chr::XFont> font1, font2;
chr::xf::FontSequence sequence1, sequence2;

FontManager provides font loading functionality. XFont is our font object. FontSequence stores transformed glyphs and send them to the GPU when required.

Sketch.cpp

Inside setup()

font1 = fontManager.getFont(InputSource::resource("Helvetica_Regular_64.fnt"), XFont::Properties2d());
font1->setShader(textureAlphaShader);
font1->setSize(20);
font1->setColor(0, 0, 0, 1);

font2 = fontManager.getFont(InputSource::resource("Georgia_Regular_64.fnt"), XFont::Properties2d());
font2->setShader(textureAlphaShader);
font2->setSize(40);
font2->setColor(0.75f, 0, 0, 1);

We load 2 fonts: Helvetica and Georgia and declare that we’re going to use them for 2d. The .fnt format is used for these files (another article will explain how to generate font files.)

Each XFont is using a texture atlas in GL_ALPHA format, containing glyphs. Since one single texture is used by font, it’s very efficient for rendering (otherwise, swapping between different textures can be costly OpenGL wise.)

Then, we assign the fonts a TextureAlphaShader stock shader. Finally, we assign a size and a color.

Inside resize()

lineBatch.clear();
drawGuides();
drawCircle(windowInfo.center(), RADIUS);

drawText1();

First, we fill lineBatch with the guides and the circle. If you have read the previous articles, you should understand how it works.

Then, we call drawText1():

font1->beginSequence(sequence1);

drawText(*font1, u"bottom-left", PADDING, windowInfo.height - PADDING);

drawAlignedText(*font1, u"top-left", glm::vec2(PADDING, PADDING), XFont::ALIGN_LEFT, XFont::ALIGN_TOP);
drawAlignedText(*font1, u"top-right", glm::vec2(windowInfo.width - PADDING, PADDING), XFont::ALIGN_RIGHT, XFont::ALIGN_TOP);
drawAlignedText(*font1, u"bottom-right", glm::vec2(windowInfo.width - PADDING, windowInfo.height - PADDING), XFont::ALIGN_RIGHT, XFont::ALIGN_BASELINE);
drawAlignedText(*font1, u"top-middle", glm::vec2(windowInfo.width / 2, PADDING), XFont::ALIGN_MIDDLE, XFont::ALIGN_TOP);
drawAlignedText(*font1, u"bottom-middle", glm::vec2(windowInfo.width / 2, windowInfo.height - PADDING), XFont::ALIGN_MIDDLE, XFont::ALIGN_BASELINE);

Matrix matrix;
matrix
  .translate(PADDING, windowInfo.height / 2)
  .rotateZ(-HALF_PI);
drawAlignedText(*font1, u"middle-left", matrix, XFont::ALIGN_MIDDLE, XFont::ALIGN_TOP);

matrix
  .setTranslate(windowInfo.width - PADDING, windowInfo.height / 2)
  .rotateZ(HALF_PI);
drawAlignedText(*font1, u"middle-right", matrix, XFont::ALIGN_MIDDLE, XFont::ALIGN_TOP);

font1->endSequence();

This is for drawing the 8 surrounding pieces of text. We begin a FontSequence, fill it with 8 groups of transformed glyphs and then end the sequence. Then, it’s possible to send the sequence to the GPU (this is done inside draw().)

Let’s start by explaining how the drawText() function works:

void Sketch::drawText(XFont &font, const u16string &text, float x, float y)
{
  for (auto c : text)
  {
    auto glyphIndex = font.getGlyphIndex(c);
    font.addGlyph(glyphIndex, x, y);
    x += font.getGlyphAdvance(glyphIndex);
  }
}

Note that we pass a u16string for the text. The text engine is using this format, because it’s much easier to manipulate than utf-8 (used by the regular C++ string.)

Then, we iterate over each character in the text:

  • We get the glyph index, which is a number used internally by the text engine. If this number equals to -2, it corresponds to space. If it’s equals to -1, it means that there is no corresponding glyph.
  • We add the glyph (to the sequence) at the x,y position.
  • We increment x by the width taken by the glyph.

That’s it. Now let’s examine the first drawAlignedText() function:

void Sketch::drawAlignedText(XFont &font, const u16string &text, const glm::vec2 &position, XFont::Alignment alignX, XFont::Alignment alignY)
{
  auto newPosition = position + font.getOffset(text, alignX, alignY);
  drawText(font, text, newPosition.x, newPosition.y);
}

This is for drawing text at a specific position, with a specific alignment. We compute the new position using the getOffset() method and then we call the regular drawText() function.

Now, the question is: how to draw rotated text? (i.e. for “middle-left” and “middle-right”.) The solution is to use a 4x4 Matrix: we translate it to the required center and rotate it, in this case either by -90 degrees or 90 degrees. Then we use the second drawAlignedText() function:

void Sketch::drawAlignedText(XFont &font, const u16string &text, Matrix &matrix, XFont::Alignment alignX, XFont::Alignment alignY)
{
  auto offset = font.getOffset(text, alignX, alignY);

  for (auto c : text)
  {
    auto glyphIndex = font.getGlyphIndex(c);
    font.addGlyph(matrix, glyphIndex, offset.x, offset.y);
    offset.x += font.getGlyphAdvance(glyphIndex);
  }
}

Inside draw()

The first relevant part is:

font1->replaySequence(sequence1);

It has the effect of sending the surrounding text to the GPU. Then we take care of the circular text:

font2->beginSequence(sequence2);
drawCircularText(*font2, TEXT, windowInfo.center(), RADIUS, -clock()->getTime() * 100.0f, XFont::ALIGN_MIDDLE);
font2->endSequence();
font2->replaySequence(sequence2);

Since the text must be animated indefinitely, we must update our sequence at each frame inside draw(). We simulate motion on the circle by using an offset which is growing indefinitely. Let’s take a look at the drawCircularText() function:

void Sketch::drawCircularText(chr::XFont &font, const std::u16string &text, const glm::vec2 &position, float radius, float offset, XFont::Alignment alignY)
{
  float offsetY = font.getOffsetY(alignY);
  Matrix matrix;

  for (auto c : text)
  {
     auto glyphIndex = font.getGlyphIndex(c);
     float halfWidth = font.getGlyphAdvance(glyphIndex) / 2;
     offset += halfWidth;

     if (glyphIndex >= 0)
     {
       float d = offset / radius;
       matrix
         .setTranslate(position.x + sinf(d) * radius, position.y - cosf(d) * radius)
         .rotateZ(d);

       font.addGlyph(matrix, glyphIndex, -halfWidth, offsetY);
    }

    offset += halfWidth;
  }
}

Here also, we use a 4x4 Matrix. For each character in the text, we translate it to the right offset on the circle and rotate it to the right angle. The idea of using halfWidth is to properly draw the glyph from its horizontal center.