== 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 what printing text actually costs. 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 `colorForeground` is a setting for drawing text and lines, and a sprite is drawn with its own pixels. If your carefully coloured title comes out white, this is why. A sprite can be recoloured after it is made, but by a sprite call, not by `colorForeground`. `spriteSetTint(image, red, green, blue)` multiplies every pixel of the sprite by that colour each time it is drawn, so text rendered in white can be drawn yellow today and red tomorrow without being made again. White is the colour to render in when you mean to tint: anything multiplied by white stays what it was, so the tint is exactly the colour you ask for. === What Printing Costs The first time `fontPrint` sees a string, it does real work: it rasterizes every letter from the font's outlines and builds an image of the whole string. Then it keeps that image. The next time you print the same string, with the same font, quality, and colours, it skips all of that and copies the image it already has, which is about what drawing a sprite costs. A string you stop printing is let go a second later, so nothing piles up. That means the obvious way to write a title screen is also the fast one. Printing `"ASTEROID PATROL"` sixty times a second renders it once. A score that changes twice a second renders twice a second, on its own, with no bookkeeping from you. The one case that costs is text that is *different* every frame -- a timer in milliseconds, a frame counter -- because every frame is a string `fontPrint` has never seen. On a desktop it does not matter. On a Raspberry Pi or a handheld, a screenful of it will. The cure is to change the text less often: a timer in whole seconds is a new string once a second, not sixty times. So why did this lesson use `fontToSprite` at all? Because a sprite can do things printed text cannot: * *It can be measured*, which is the only way to centre text or line it up against the right hand side. * *It can be scaled and turned* with the `sprite*` calls from lesson nine. * *It can outlive its font*, which matters in a moment. Use `fontPrint` for text, and `fontToSprite` when you need one of those. The score in this lesson needs one of them: it sits against the right hand margin, so it has to be measured, so it has to be a sprite. And a sprite of a score is only right until the score changes. When it does, render a new sprite and unload the old one -- forgetting that 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 ---- `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 and the hint sit centred, the score climbs against the right margin and is re-rendered only when it changes, the hint blinks, and the label and the clock are printed with `fontPrint` every frame -- the clock in whole seconds, so it is a new string only once 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. Then put it back, make the hint white instead, and turn it yellow with `spriteSetTint` just before it is drawn. . *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, but it now renders the score sixty times a second instead of when it changes -- exactly the work `fontPrint` saves you when you let it. Then take out the `spriteUnload` in `renderScore` as well, leave it running for a minute, and watch the memory your game is using. . *Print a clock in milliseconds.* Change the clock line to print `singeGetTicks()` without the `// 1000`. It still works; it is now a string `fontPrint` has never seen, every frame. On a slow machine, that is the kind of text that costs. . *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` keeps what it renders, so printing the same text every frame is cheap; text that is different every frame is what costs. * `fontToSprite` renders into a sprite, which `spriteGetWidth` measures and the `sprite*` calls can scale and turn. Use it for that, not for speed. * Measuring is how you centre or right align text, because nothing else can tell you how wide a string will be. * `colorForeground` reaches text when it is made, never a sprite when it is drawn; `spriteSetTint` is how a sprite is recoloured afterwards. * `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.