Hello Scene — It’s all about the text
That’s a lot of fonts. But, it’s a relatively simple task to achieve once we’ve gained some understanding of how to deal with text. We’ll park this bit of code here (fontlist.cpp) while we gain some understanding.
#include "gui.h" #include "fontmonger.h" std::list<std::string> fontList; void drawFonts() { constexpr int rowHeight = 24; constexpr int colWidth = 213; int maxRows = canvasHeight / rowHeight; int maxCols = canvasWidth / colWidth; int col = 0; int row = 0; std::list<std::string>::iterator it; for (it = fontList.begin(); it != fontList.end(); ++it) { int x = col * colWidth; int y = row * rowHeight; textFont(it->c_str(), 18); text(it->c_str(), x, y); col++; if (col >= maxCols) { col = 0; row++; } } } void setup() { setCanvasSize(1280, 1024); FontMonger::collectFontFamilies(fontList); background(PixelRGBA(0xffdcdcdc)); drawFonts(); }
I must say, dealing with fonts, and text rendering is one of the most challenging of the graphics disciplines. We could spend years and gigabytes of text explaining the intricacies of how fonts and text work. For our demo scene, we’re not going to get into all that though. We just want a little bit of text to be able to splash around here and there. So, I’m going to go the easy route, and explain how to use the system text rendering and incorporate it into the rest of our little demo framework.
First of all, some terminology. These words; Font, Font Face, OpenType, Points, etc, are all related to fonts, and all can cause confusion. So, let’s ignore all that for now, and just do something simple.
And the code to make it happen?
#include "gui.h" void setup() { setCanvasSize(320, 240); background(PixelRGBA (0xffffffff)); // A white background text("Hello Scene!", 24, 48); }
Pretty simple right? By default, the demo scene chooses the “Segoe UI” font at 18 pixels high to do text rendering. The single call to “text(…)”, puts whatever text you want at the x,y coordinates specified afterward. So, what is “Segoe UI”? A Font describes the shape of a character. So, the letter ‘A’ in one font looks one way in say “Times New Roman”, and probably slightly different in “Tahoma”. These are stylistic differences. Us humans will just recognize it as ‘A’. Each font contains a bunch of descriptions of how to draw individual characters. These descriptions are essentially just polygons, with curves, and straight lines.
I’m grossly simplifying.
The basic description can be scaled, rotated, printed in ‘bold’, ‘italics’, or ‘underline’, depending on what you want to do when you’re displaying text. So, besides just saying where we want text to be located, we can specify the size (in pixels), and choose a specific font name other than the default.
Which was produced with a slight change in the code
#include "gui.h" void setup() { setCanvasSize(640, 280); background(PixelRGBA (0xffffffff)); // A white background textFont("Sitka Text", 100); text("Hello My Scene!", 24, 48); }
And last, you can change the color of the text
How exciting is that?! For the simplest of demos, and maybe even some UI framework, this might be enough. But, le’ts go a little bit further, and get some more functions that might be valuable.
First thing, we need to understand a little bit more about the font, like how tall and wide characters are, where’s the baseline, the ascent, and descent. Character width and height are easily understood. Ascent and descent might not be as well understood. Let’s start with a little display.
Some code to go with it
#include "gui.h" constexpr int leftMargin = 24; constexpr int topMargin = 24; void drawTextDetail() { // Showing font metrics const char* str2 = "My Scene!"; PixelCoord sz; textMeasure(sz, str2); constexpr int myTop = 120; int baseline = myTop + fontHeight - fontDescent; int topline = myTop + fontLeading; strokeRectangle(*gAppSurface, leftMargin, myTop, sz.x(), sz.y(), PixelRGBA(0xffff0000)); // Draw internalLeading - green copySpan(*gAppSurface, leftMargin, topline, sz.x(), PixelRGBA(0xff00ff00)); // draw baseline copySpan(*gAppSurface, leftMargin, baseline, sz.x(), PixelRGBA(0xff0000ff)); // Draw text in the box // Turquoise Text textColor(PixelRGBA(0xff00ffff)); text("My Scene!", leftMargin, myTop); } void setup() { setCanvasSize(640, 280); background(PixelRGBA (0xffffffff)); textFont("Sitka Text", 100); drawTextDetail(); }
In the setup, we do the usual to create a canvas of a desirable size. Then we select the font with a particular pixel height. Then wave our hands and call ‘drawDetail()’.
In ‘drawDetail()’, one of the first calls is to ‘textMeasure()’. We want the answer to; “How many pixels wide and high is this string?” The ‘textMeasure()’ function does this. It’s pretty straight forward as the GDI API that we’re using for text rendering has a function call for this purpose.
void textMeasure(PixelCoord& pt, const char* txt) { SIZE sz; ::GetTextExtentPoint32A(gAppSurface->getDC(), txt,strlen(txt), &sz); pt[0] = sz.cx; pt[1] = sz.cy; }
It’s that simple. Just pass in a structure to receive the size, and make the call to ‘GetTextExtentPoint32A()’. I choose to return the value in a PixelCoord object, because I don’t want the Windows specific data structures bleeding into my own demo API. This allows me to change the underlying text API without having to worry about changing dependent data structures.
The size that is returned incorporates a few pieces of information. It’s not a tight fit to the string. The size is derived from a combination of global font information (tallest character, lowest part of a character), as well as the cumulative widths of the actual characters specified. In the case of our little demo, the red rectangle represents the size that was returned.
There are a couple more bits of information that are set when you select a font of a particular size. The three most important bits are, the ascent, descent, and internal leading.
Let’s start from the descent. Represented by the blue line, this is the maximum amount any given character of the font might fall below the ‘baseline’. The baseline is implicitly defined by this descent, and it essentially the fontHeight-fontDescent. This is the line where all the other characters will have as their ‘bottom’. The ‘ascent’ is the amount of space above this baseline. So, the total fontHeight is the fontDescent+fontAscent. The ascent isn’t explicitly shown, because it is essentially the topline of the rectangle. The last bit is the internalLeading. This is the amount of space used by accent characters and the like. The fontLeading is this number, and is represented as the green line, as it’s essentially subtracted from the fontHeight in terms of coordinates.
And there you have it. All the little bits and pieces of a font. When you specify a location for drawing the font in the ‘text()’ function, you’re essentially specifying the top left corner of this red rectangle. Of course, that leaves you a bit high and dry when it comes to precisely placing your text. More than likely, what you really want to do is place your text according to the baseline, so that you can be more assured of where your text is actually going to show up. Maybe you want that, maybe you don’t. What you really need is the flexibility to specify the ‘alignment’ of your text rendering.
This is actually a re-creation of something I did about 10 years ago, for another project. It’s a pretty simple matter once you have adequate font and character sizing information.
#include "gui.h" #include "textlayout.h" TextLayout tLayout; void drawAlignedText() { int midx = canvasWidth / 2; int midy = canvasHeight / 2; // draw vertical line down center of canvas line(*gAppSurface, midx, 0, midx, canvasHeight - 1, PixelRGBA(0xff000000)); // draw horizontal line across canvas line(*gAppSurface, 0, midy, canvasWidth - 1, midy, PixelRGBA(0xff000000)); tLayout.textFont("Consolas", 24); tLayout.textColor(PixelRGBA(0xff000000)); tLayout.textAlign(ALIGNMENT::LEFT, ALIGNMENT::BASELINE); tLayout.text("LEFT", midx, 24); tLayout.textAlign(ALIGNMENT::CENTER, ALIGNMENT::BASELINE); tLayout.text("CENTER", midx, 48); tLayout.textAlign(ALIGNMENT::RIGHT, ALIGNMENT::BASELINE); tLayout.text("RIGHT", midx, 72); tLayout.textAlign(ALIGNMENT::RIGHT, ALIGNMENT::BASELINE); tLayout.text("SOUTH EAST", midx, midy); tLayout.textAlign(ALIGNMENT::LEFT, ALIGNMENT::BASELINE); tLayout.text("SOUTH WEST", midx, midy); tLayout.textAlign(ALIGNMENT::RIGHT, ALIGNMENT::TOP); tLayout.text("NORTH EAST", midx, midy); tLayout.textAlign(ALIGNMENT::LEFT, ALIGNMENT::TOP); tLayout.text("NORTH WEST", midx, midy); } void setup() { setCanvasSize(320, 320); tLayout.init(gAppSurface); background(PixelRGBA(0xffDDDDDD)); drawAlignedText(); }
Design-wise, I chose to stuff the various text measurement and rendering routines into a separate object. My other choice would have been to put them into the gui.h/cpp file, and I did do that initially, but then I thought better of it, because that would be forcing a particular strong opinion on how text should be dealt with, and I did not make that choice for drawing in general, so I thought better of it and chose to encapsulate the text routines in this layout structure (textlayout.h) .
Now that we have the ability to precisely place a string, we can get a little creative in playing with the displacement of all the characters in a string. With that ability, we can have text placed based on the evaluation of a function, with animation of course.
#include "gui.h" #include "geotypes.hpp" #include "textlayout.h" using namespace alib; constexpr int margin = 50; constexpr int FRAMERATE = 20; int dir = 1; // direction int currentIteration = 1; // Changes during running int iterations = 30; // increase past frame rate to slow down bool showCurve = true; TextLayout tLayout; void textOnBezier(const char* txt, GeoBezier<ptrdiff_t>& bez) { int len = strlen(txt); double u = 0.0; int offset = 0; int xoffset = 0; while (txt[offset]) { // Isolate the current character char buf[2]; buf[0] = txt[offset]; buf[1] = 0; // Figure out the x and y offset auto pt = bez.eval(u); // Display current character tLayout.text(buf, pt.x(), pt.y()); // Calculate size of current character // so we can figure out where next one goes PixelCoord charSize; tLayout.textMeasure(charSize, buf); // Now get the next value of 'u' so we // can evaluate where the next character will go u = bez.findUForX(pt.x() + charSize.x()); offset++; } } void strokeCurve(PixelMap& pmap, GeoBezier<ptrdiff_t> &bez, int segments, const PixelRGBA c) { // Get starting point auto lp = bez.eval(0.0); int i = 1; while (i <= segments) { double u = (double)i / segments; auto p = bez.eval(u); // draw line segment from last point to current point line(pmap, lp.x(), lp.y(), p.x(), p.y(), c); // Assign current to last lp = p; i = i + 1; } } void onFrame() { background(PixelRGBA(0xffffffff)); int y1 = maths::Map(currentIteration, 1, iterations, 0, canvasHeight); GeoCubicBezier<ptrdiff_t> bez(margin, canvasHeight / 2, canvasWidth * 0.25, y1, canvasWidth - (canvasWidth * 0.25), canvasHeight -y1, canvasWidth - margin, canvasHeight / 2.0); if (showCurve) strokeCurve(*gAppSurface, bez, 50, PixelRGBA(0xffff0000)); // honor the character spacing tLayout.textColor(PixelRGBA(0xff0000ff)); textOnBezier("When Will The Quick Brown Fox Jump Over the Lazy Dogs Back", bez); currentIteration += dir; // reverse direction if needs be if ((currentIteration >= iterations) || (currentIteration <= 1)) dir = dir < 1 ? 1 : -1; } void setup() { setCanvasSize(800, 600); setFrameRate(FRAMERATE); tLayout.init(gAppSurface); tLayout.textFont("Consolas", 24); tLayout.textAlign(ALIGNMENT::CENTER, ALIGNMENT::CENTER); } void keyReleased(const KeyboardEvent& e) { switch (e.keyCode) { case VK_ESCAPE: halt(); break; case VK_SPACE: showCurve = !showCurve; break; case 'R': recordingToggle(); break; } }
For once, I won’t go line by line. The key trick here is the ‘findUForX()’ function of the bezier object. Since textMeasure() tells us how wide a string is (in pixels), we know how much to advance in the x direction as we display characters. Our bezier curve has an eval() function, which takes a value from 0.0 to 1.0. It will return a ‘y’ value along the curve when given a ‘u’ value between 0 and 1 to evaluate. So, we want to match the x offset of the next character with its corresponding ‘u’ value along the curve, then we can evaluate the curve at that position, and find out the appropriate ‘y’ value.
Notice in the setup, the text alignment is set to CENTER, CENTER. This means that the coordinate positions being calculated should represent the center of the characters being printed. That roughly leaves the center of the character aligned with the evaluated values of the curve, which will match your expectations most closely. Another way to do it might be to do LEFT, BASELINE, to get the characters left aligned, and use the curve as the baseline. There are a few possibilities, and you can simply choose what suits your needs.
This is a very crude way to do some displayment of text on a curve, but, showing text along a path is a fairly common parlor trick in demo applications, and this is one way to doing it quick and dirty. Your curves doesn’t have to be a bezier, it could be anything you like. Just take it one character at a time, and use the textAlignment, and see what you can accomplish.
There is a design choice here. I am using simple GDI based interfaces to display the text. I can do this because at the core, the PixelArray that we’re drawing into does in fact have a “DeviceContext”, so GDI knows how to draw into it. This is a great convenience, because it means that we can do all the independent drawing that we’ve been doing, from random pixels to bezier curves, and when we get to something we can’t quite handle, we can fall back to what the system provides, in this case text rendering.
With that, we’re at the end of this series. We’ve gone from a basic window on the screen, to drawing text along an animating bezier curve, all while recording to a .mpg file. This is just the beginning. We’ve covered some design choices along the way, including the desire to keep the code small and composable. The only thing left to do is go out and create something of your own, by using this kind of toolkit, or better yet, have the confidence to create your own.
The Demo Scene is out there. Go create something.