== Lesson 19: Text That Looks Good image::learn/19-text.png[The finished lesson, 480] Every word you have put on screen since lesson one came out of `overlayPrint`, and `overlayPrint` has exactly one font: a fixed-width block of pixels built into the engine, the same size forever, in whatever colour it happens to be. It is perfect for a frame counter. It is wrong for a title, a score, a menu, or anything a player is meant to look at rather than read past. This lesson loads a real font off disk, prints with it, colours it, measures it so you can centre things, and shows you the one performance trap that catches everybody. At the end you will have a title screen that looks deliberate instead of accidental. === Set the Overlay to a Size You Can Think In Start a new file called `text.singe` with two lines: [source,lua] ---- dofile("Singe/Framework.singe") overlaySetResolution(discGetWidth(), discGetHeight()) ---- The first line you have written many times; it loads the framework, which defines `DIR`, the `SWITCH_*` names, and the `FONT_QUALITY_*` names you are about to use. The second matters more than it looks. The overlay -- the surface everything you draw lands on -- starts at *half* the size of the world. A game with no video has a 720 by 480 canvas, so the overlay starts at 360 by 240, and a 36 point font on a 360 pixel wide overlay is enormous. `overlaySetResolution` replaces the overlay with one of the size you ask for, and from then on every coordinate you give, and everything `overlayGetWidth` and `overlayGetHeight` tell you, is in that space. `discGetWidth()` and `discGetHeight()` answer the size of the video when there is one and the size of the canvas when there is not, so that one line gives you an overlay that matches the world whichever kind of game this is. Call it once, at the start, before you draw anything. === Load a Font Under that, add: [source,lua] ---- fontQuality(FONT_QUALITY_BLENDED) local titleFont = fontLoad("Singe/FreeSansBold.ttf", 48) ---- `fontLoad` opens a TrueType font file and hands you back a number -- a *handle* -- that you use whenever you want that font. The second argument is the point size, and it is fixed for the life of the handle. There is no way to change the size of a loaded font afterwards. If you want the same face at three sizes, you call `fontLoad` three times and keep three handles. `Singe/FreeSansBold.ttf` is the font the engine unpacked into the `Singe` folder in your work folder the first time you ran anything. Every copy of Singe has it, so you can rely on it. Any other `.ttf` file works the same way; put it beside your script and load it as `DIR .. "myfont.ttf"`. Now print with it: [source,lua] ---- function onOverlayUpdate() overlayClear() fontPrint(24, 60, "ASTEROID PATROL") return OVERLAY_UPDATED end ---- Run it. Big white letters near the top left. Notice what you did *not* do: you never told `fontPrint` which font to use. `fontLoad` makes the font it just loaded the selected one, and `fontPrint` always draws with whatever is selected. With more than one font loaded you choose between them with `fontSelect`, which you will do in a moment. Notice also that `24` and `60` are pixels, not character cells. This is the difference between `fontPrint` and `overlayPrint` that trips people up: `overlayPrint(2, 2, ...)` means two letters in and two lines down, while `fontPrint(2, 2, ...)` means two pixels in and two pixels down, which is jammed into the corner. The `y` you give is the *top* of the text, not the baseline it sits on. === Colour `fontPrint` takes three arguments and none of them is a colour. The colour comes from somewhere else: [source,lua] ---- colorForeground(255, 211, 90) fontPrint(24, 60, "ASTEROID PATROL") ---- `colorForeground` sets red, green, and blue from `0` to `255`, and an optional fourth number for opacity. It is not an argument to anything; it is a setting that the engine remembers. Everything drawn after it comes out in that colour until you change it again, exactly the way `overlayBox` and `overlayLine` already work. So the pattern is always the same: set the colour, then draw. This is the first thing people get wrong with fonts, and the second thing is coming up in a few pages. === Quality `fontQuality` decides how the letters are rasterized -- how the curves of a letter are turned into pixels -- and there are three choices. Try each one by putting it above the `fontLoad` line and looking at the result closely. `FONT_QUALITY_SOLID` is the fastest. Every pixel is either the letter or it is not, so the diagonals and curves come out with visible stair steps. At 48 point it looks cheap. At 12 point on a small overlay it can look *sharper* than the alternatives, which is sometimes what you want. `FONT_QUALITY_SHADED` smooths the edges, but it does it by blending them into the current background colour, and it draws that background as a solid rectangle behind the whole string. You get a label with a box around it whether you wanted one or not. It is the right answer for a debug readout over a busy picture and the wrong answer for almost everything else. `FONT_QUALITY_BLENDED` smooths the edges and keeps them see-through, so the letters sit on whatever is behind them. It costs the most and it is what you want nearly all the time. Use it unless you have a reason not to. The setting is global, not per font, and it applies from the moment you call it onwards. Sprites you have already made keep the quality they were made with. The manual's entry for `fontQuality` lists the three names and their values. === Measuring, so You Can Centre Things Sooner or later -- usually about ten minutes in -- you want the title in the middle of the screen. You know the screen is `overlayGetWidth()` wide. You do not know how wide the text is, and there is no `fontMeasure` call to ask. What there is instead is `fontToSprite`. It renders a string with the selected font, the current quality, and the current colour, and instead of putting it on screen it hands you back a *sprite*: the same kind of handle `spriteLoad` gives you, which means every `sprite*` call you learned in lesson nine works on it. Including the two that measure it. [source,lua] ---- fontSelect(titleFont) colorForeground(255, 211, 90) local titleImage = fontToSprite("ASTEROID PATROL") ---- And then, in `onOverlayUpdate`: [source,lua] ---- local x = (overlayGetWidth() - spriteGetWidth(titleImage)) / 2 spriteDraw(titleImage, x, 60) ---- That is centring, and it is the whole trick. Take the width of the space, take away the width of the thing, and half of what is left is the gap at each side. Right alignment is the same idea with less arithmetic. To put text against the right hand margin: [source,lua] ---- spriteDraw(scoreImage, overlayGetWidth() - MARGIN - spriteGetWidth(scoreImage), y) ---- `spriteGetHeight` measures the other direction, for stacking lines or centring vertically. There is a catch in that first code block, and it is the second thing everybody gets wrong. *The colour is baked in when the sprite is made, not when it is drawn.* `colorForeground` above `fontToSprite` changes the text. Putting it above `spriteDraw` instead does nothing at all, because a sprite is drawn with its own pixels and is never tinted. If your carefully coloured title comes out white, this is why. === The Trap: fontPrint Is Not Free Here is the thing the manual says plainly and everyone reads past. `fontPrint` does not draw a picture it made earlier. Every single call rasterizes the whole string from the font outlines, builds an image of it, copies that image onto the overlay, and throws it away. Then, one sixtieth of a second later, it does all of that again. For a title that has not changed since the game started, you are re-drawing the same letters from scratch sixty times a second, forever. On a desktop with three lines of text you will never notice. With a screen full of text, or on a Raspberry Pi, or in a cabinet, you will notice a great deal. The fix is the call you just met. `fontToSprite` does that work *once*. `spriteDraw` afterwards is a plain copy, which is what the hardware is for. So the rule is short: * Text that never changes -- titles, labels, instructions, menu entries -- `fontToSprite` once at startup, `spriteDraw` every frame. * Text that changes every frame and nobody is staring at -- a debug readout, a frame number -- `fontPrint` is fine. * Text that changes sometimes -- a score, a name, a timer in whole seconds -- render a new sprite when it changes, and unload the old one. That last case is worth writing out, because "unload the old one" is easy to forget and forgetting it leaks a sprite per change: [source,lua] ---- local function renderScore() if scoreImage ~= nil then spriteUnload(scoreImage) end fontSelect(scoreFont) colorForeground(255, 255, 255) scoreImage = fontToSprite(string.format("%06d", score)) scoreShown = score end ---- `scoreShown` remembers which score that sprite says, so the drawing code can tell whether it is still right: [source,lua] ---- if score ~= scoreShown then renderScore() end ---- A score changes perhaps twice a second. This renders twice a second instead of sixty times, and the code to do it is nine lines. `string.format("%06d", score)` is plain Lua, not Singe. It turns a number into a string, and `%06d` means "as a whole number, at least six digits, padded with zeros", which is why arcade scores have leading zeros. Change it to `%d` and the zeros go away. === Putting Fonts Away `fontUnload` closes a font and makes its handle invalid. Sprites you made with `fontToSprite` are not affected -- once rendered, a sprite has nothing more to do with the font -- so you can unload a font and go on drawing text made from it. One sharp edge: if you unload the font that was selected, *no* font is selected afterwards, and the next `fontPrint` ends your game with an error. Select another one first if you are going to keep printing. The tidy place for all this is `onShutdown`, a callback the engine calls once when the game is closing: [source,lua] ---- function onShutdown() spriteUnload(titleImage) fontUnload(titleFont) end ---- Singe cleans up after you when the game ends, so nothing catches fire if you skip this. Write it anyway. A game that releases what it loaded is a game you can load twice. === The Whole Script [source,lua] ---- dofile("Singe/Framework.singe") overlaySetResolution(discGetWidth(), discGetHeight()) local MARGIN = 24 local TITLE_Y = 60 local SCORE_Y = 170 local LABEL_Y = 186 local HINT_Y = 300 local CLOCK_Y = 430 local score = 0 local titleFont = nil local scoreFont = nil local smallFont = nil local titleImage = nil local hintImage = nil local scoreImage = nil local scoreShown = nil local function centred(image) return (overlayGetWidth() - spriteGetWidth(image)) / 2 end local function renderScore() if scoreImage ~= nil then spriteUnload(scoreImage) end fontSelect(scoreFont) colorForeground(255, 255, 255) scoreImage = fontToSprite(string.format("%06d", score)) scoreShown = score end fontQuality(FONT_QUALITY_BLENDED) titleFont = fontLoad("Singe/FreeSansBold.ttf", 48) scoreFont = fontLoad("Singe/FreeSansBold.ttf", 36) smallFont = fontLoad("Singe/FreeSansBold.ttf", 18) fontSelect(titleFont) colorForeground(255, 211, 90) titleImage = fontToSprite("ASTEROID PATROL") fontSelect(smallFont) colorForeground(140, 200, 255) hintImage = fontToSprite("Press the fire button to score") renderScore() function onInputPressed(what) if what == SWITCH_BUTTON1 then score = score + 125 end end function onOverlayUpdate() overlayClear() spriteDraw(titleImage, centred(titleImage), TITLE_Y) if score ~= scoreShown then renderScore() end spriteDraw(scoreImage, overlayGetWidth() - MARGIN - spriteGetWidth(scoreImage), SCORE_Y) fontSelect(smallFont) colorForeground(160, 160, 160) fontPrint(MARGIN, LABEL_Y, "SCORE") fontPrint(MARGIN, CLOCK_Y, "Running for " .. (singeGetTicks() // 1000) .. " seconds") if (singeGetTicks() // 500) % 2 == 0 then spriteDraw(hintImage, centred(hintImage), HINT_Y) end return OVERLAY_UPDATED end function onShutdown() spriteUnload(titleImage) spriteUnload(hintImage) if scoreImage ~= nil then spriteUnload(scoreImage) end fontUnload(titleFont) fontUnload(scoreFont) fontUnload(smallFont) end ---- Run it and hold the fire button. The title sits centred and never re-renders, the score climbs and re-renders only when it changes, the hint blinks, and the clock at the bottom is the one thing honestly drawn with `fontPrint` sixty times a second. === What Just Happened [source,lua] ---- titleFont = fontLoad("Singe/FreeSansBold.ttf", 48) scoreFont = fontLoad("Singe/FreeSansBold.ttf", 36) smallFont = fontLoad("Singe/FreeSansBold.ttf", 18) ---- One file, three sizes, three handles. Each one costs memory for its own set of rendered glyphs, so load the sizes you use and no more. Three is normal. A dozen is a smell. After these three lines the selected font is `smallFont`, because loading selects. That is why the code calls `fontSelect(titleFont)` before rendering the title: the last thing loaded is not the thing you want first. [source,lua] ---- local function centred(image) return (overlayGetWidth() - spriteGetWidth(image)) / 2 end ---- Written once as a function because it is used twice, and because the next time you want something centred you will want it again. Give the arithmetic a name and you never have to read it again. [source,lua] ---- if (singeGetTicks() // 500) % 2 == 0 then spriteDraw(hintImage, centred(hintImage), HINT_Y) end ---- `singeGetTicks()` is the number of milliseconds since the engine started. `//` is division that throws away the remainder, so `singeGetTicks() // 500` counts half-seconds, and `% 2` is the remainder after dividing by two, which is `0`, `1`, `0`, `1`... So the hint is drawn for half a second and skipped for half a second. Drawing nothing is how you make something blink. [source,lua] ---- fontSelect(smallFont) colorForeground(160, 160, 160) fontPrint(MARGIN, LABEL_Y, "SCORE") ---- Both settings are needed here, and it is worth being clear about why. Drawing a sprite does not change the selected font or the colour, so the two `spriteDraw` calls above leave both exactly as they found them. But the setup code selected three different fonts in turn, and `renderScore` selects another one whenever the score moves, so the selection when this line runs is whichever one happened to be chosen last. Do not try to keep track of it. Select the font and set the colour immediately before you print, every time. It is two lines and it removes a whole category of puzzled evening. === Try It . *Change the quality.* Put `FONT_QUALITY_SOLID` in the `fontQuality` line and look at the curve of the `S` in `ASTEROID`. Then try `FONT_QUALITY_SHADED` and watch what happens behind every string. . *Make the hint yellow.* Move the `colorForeground(140, 200, 255)` line from above `fontToSprite` to just above the `spriteDraw` that draws `hintImage`, and change the numbers. Work out why nothing happens. . *Right align the clock.* The clock is drawn with `fontPrint`, which cannot be measured. Get it against the right margin anyway. There is only one way, and finding it is the point. . *Take out the `scoreShown` check*, so `renderScore()` runs every frame instead. It will look identical. Leave it running for a minute and watch the memory your game is using. . *Use your own font.* Find a `.ttf` on your machine, copy it beside your script, and load it with `DIR .. "thatfont.ttf"`. Some fonts look terrible at 18 point and fine at 48. That is the font's fault, not yours. === Break It on Purpose Add a line reading `fontPrint(24, 60, "SCORE")` directly under `overlaySetResolution(discGetWidth(), discGetHeight())`, so that it runs before any `fontLoad` has. Singe starts and dies at once: ---- 5:fontPrint: No font selected. ---- Read it the same way as any other error. `5` is the line. `fontPrint` is which engine call complained -- this shape of message comes from inside the engine rather than from Lua, so it names the function instead of the file. The rest is the complaint, and here it is exact: you asked it to print and there was no font to print with. You will meet this one for real in a less obvious way. Unload the selected font while the game is running and the next `fontPrint` says precisely the same thing, several hundred lines away from the `fontUnload` that caused it. === What You Learned * `fontLoad` opens a TrueType file at one fixed point size and returns a handle. One size, one handle. * Loading a font selects it; `fontSelect` chooses between loaded fonts. * `fontPrint` counts in pixels and `overlayPrint` counts in character cells. * Colour is not an argument. `colorForeground` is a setting; set it, then draw. * `fontQuality` picks how letters are rasterized, and `FONT_QUALITY_BLENDED` is the usual answer. * `fontPrint` re-renders the string on every single call. * `fontToSprite` renders once into a sprite, which `spriteDraw` then copies cheaply and `spriteGetWidth` measures. * Measuring is how you centre or right align text, because nothing else can tell you how wide a string will be. * A sprite's colour is fixed when it is made, not when it is drawn. * `fontUnload` frees a font; the sprites it made survive it. === Next Time Text that looks good is a long way from a menu that works. A page with a title, a list you move a cursor through, a slider, and a button is a hundred lines of drawing and cursor arithmetic if you build it out of the calls in this lesson. Lesson twenty does not build it out of those calls at all.