In this post, I present text rendering with different styles (bold, italic, underline, ...). Some of them are straightforward to implement thanks to Pygame; the others are more tricky!
This post is part of the OpenGL 2D Facade series
We wish to add the following text styles to the facade:
For each style, we compute a dedicated tileset. For instance, for the outline case, we create tiles with outlined characters. Then, during the OpenGL rendering, the process is as before.
TextLayer class, we define constants for each text style:
class TextLayer(Layer): PLAIN = 0 BOLD = 1 ITALIC = 2 UNDERLINE = 4 SHADOW = 8 OUTLINE = 16
They are powers of two, so we can combine them using the or operator. For instance, if we wish to use bold, italic, and shadow:
style = TextLayer.BOLD | TextLayer.ITALIC | TextLayer.SHADOW
We can tell if a combination uses a style using to the and operator:
if style & Text.BOLD: print("Bold is enabled") else: print("Bold is not enabled")
The bold style is the easiest to implement because Pygame can render it. We only need to enable it once we created the font:
font = pygame.font.Font(fontName, fontSize) if style & TextLayer.BOLD: font.set_bold(True)
All that we did in the previous post can remain as before.
Pygame also renders italic characters:
font = pygame.font.Font(fontName, fontSize) if style & TextLayer.ITALIC: font.set_italic(True)
However, this solution is not sufficient: the italic characters are wider, and when placed side by side, they are too far apart.
We can solve this using the character advance, which is given by the Pygame font metrics:
metrics = font.metrics(character) advance = metrics
We save this advance for each character in a new
advance attribute in the
When we define the quads in the
setText() method of the
OpenGLTextLayer class, we increase the current
x value with the advance rather than with the tile width (see the last line):
x0 = x # Characters to display for charIndex, character in enumerate(content): if character == "\n": x = x0 y += self.tileHeight continue tile = self.__tileset.getCharacterTile(character) self.__setQuadTile(charIndex, tile.x1, tile.y1, tile.x2, tile.y2) self.__setQuadLocation(charIndex, x, y, x + tile.width, y + tile.height) x += tile.advance
Pygame also handles underline text, but the result was not right in my experiments. So, I propose to render it ourselves.
We can get underlined text by drawing a line:
if style & TextLayer.UNDERLINE: width = surface.get_width() start = (0, underlineShift) end = (width - 1, underlineShift) pygame.draw.line(surface, color, start, end, underlineThickness)
unlineShift variable is the vertical location of the line. We could use the font ascent provided by Pygame, but for many fonts, I got terrible results. I propose to compute it using a percentage of the tile height, for instance, 90%:
underlineShift = (tileHeight * 90) // 100
The facade user can also set it, so she/he can get the result she/he wants.
color variable is the color of the font, as chosen by the facade user.
underlineThickness variable contains the thickness of the line. We can also compute it using a percentage of the tile:
underlineThickness = (tileHeight * 5) // 100 if underlineThickness < 1: underlineThickness = 1
For the shadow style, we need to draw each character twice: one with the font color and another with the shadow color. As for the other cases, these drawing are during the tileset creation:
# Normal character drawing surface = font.render(character, False, color) # Create a new, larger surface size = (surface.get_width() + shadowShift, surface.get_height() + shadowShift) shadowedSurface = pygame.Surface(size, flags=pygame.SRCALPHA) # Render the character with the shadow color surface2 = font.render(character, False, shadowColor) # Blit the shadow and the character shadowedSurface.blit(surface2, (shadowShift, shadowShift)) shadowedSurface.blit(surface, (0, 0))
Note that we also use the font advance to compute characters' location on the screen (as with italic style).
Also, note that, for the combination of shadow and underline styles, we have to also render the line in the shadow (not shown in this piece of code, have a look at the final program).
The outline is the most difficult one. There are several ways to get it; I propose one of them.
The idea of this approach is to create new pixels around existing ones:
def outlineStep(surface: pygame.Surface, outlineColor: (int, int, int, int)) -> pygame.Surface: result = surface.copy() w = surface.get_width() h = surface.get_height() for y in range(h): for x in range(w): # Copy opaque pixels color = surface.get_at((x, y)) if color != 0: result.set_at((x, y), color) continue # If any pixel around the current one is opaque, then light it if (x > 0 and surface.get_at((x - 1, y)) != 0) \ or (x < w - 1 and surface.get_at((x + 1, y)) != 0) \ or (y > 0 and surface.get_at((x, y - 1)) != 0) \ or (y < h - 1 and surface.get_at((x, y + 1)) != 0) \ or (x > 0 and y > 0 and surface.get_at((x - 1, y - 1)) != 0) \ or (x > 0 and y < h - 1 and surface.get_at((x - 1, y + 1)) != 0) \ or (x < w - 1 and y > 0 and surface.get_at((x + 1, y - 1)) != 0) \ or (x < w - 1 and y < h - 1 and surface.get_at((x + 1, y + 1)) != 0): result.set_at((x, y), outlineColor) return result
We copy existing pixels (alpha is not zero) on a new surface (lines 9-11). For other pixels, if there is at least one opaque pixel around, we create a new one with the outline color (lines 13-21).
We can repeat this procedure to get larger outlines.
The creation of an outlined character is as follow:
# Normal character drawing surface = font.render(character, False, color) # Create a new, larger surface size = (surface.get_width() + 2 * thickness, surface.get_height() + 2 * thickness) outlineSurface = pygame.Surface(size, flags=pygame.SRCALPHA) # Blit the character outlineSurface.blit(surface, (thickness, thickness)) # Draw the outline for step in range(thickness): outlineSurface = outlineStep(outlineSurface, outlineColor + (255, ))
We render the character (line 2) and create a larger surface (lines 5-6). We blit the character on this surface with a shift that depends on the outline (line 9). Finally, we draw the outline (line 12-13). Each step adds another pixel to the outline.
In the next post, we'll see how to render text with mixed styles.