Have an outline of text in Pygame

Viewed 1075

So I have this function in which it puts text on the screen:

def text_speech(font : str ,size : int,text : str,color,x,y, bold : bool):
    SCREEN = width, height = 900, 600
    font = pygame.font.Font(font,size)
    font.set_bold(bold)
    text = font.render(text, True, color)
    textRect = text.get_rect()
    textRect.center = (x,y)
    screen.blit(text,textRect)

If I do this:

screen.fill((0,0,0))
text_speed('arialnarrow.ttf', 40, 'Hello', (255,255,255), (width/2), (height/2), False)

It generates the world 'Hello' on a black screen with white text. Is it possible that if the user hovers their mouse over this, it creates a red (255,0,0) outline?

2 Answers

To accomplish an outline you have to blit the multiple times. Render the text in the outline color (red):

outlineSurf = font.render(text, True, (255, 0, 0))
outlineSize = outlineSurf.get_size()

Create a surface which is grater than the text surface. The width and the height have to be increased by the doubled outline thickness:

textSurf = pygame.Surface((outlineSize[0] + outline*2, outlineSize[1] + 2*outline))
textRect = textSurf.get_rect()

Blit the outline surface 8 times on the text surface, shifted by the outline thickness (horizontal, vertical and diagonal:

offsets = [(ox, oy) 
    for ox in range(-outline, 2*outline, outline)
    for oy in range(-outline, 2*outline, outline)
    if ox != 0 or ox != 0]
for ox, oy in offsets:   
    px, py = textRect.center
    textSurf.blit(outlineSurf, outlineSurf.get_rect(center = (px+ox, py+oy))) 

Render the text with the text color and convert the surface to a per pixel alpha format (convert_alpha):

innerText = font.render(text, True, color).convert_alpha()

Blit the text in the middle of textSurf:

textSurf.blit(innerText, innerText.get_rect(center = textRect.center)) 

Blit textSurf onto the window:

textRect.center = (x,y)
screen.blit(textSurf, textRect)

See the example:

import pygame
import pygame.font

pygame.init()

width, height = 400, 300
screen = pygame.display.set_mode((width, height))
clock = pygame.time.Clock()
textRect = pygame.Rect(0, 0, 0, 0)

def text_speech(font : str, size : int, text : str, color, x, y, bold : bool, outline: int):
    global textRect 
    # font = pygame.font.Font(font,size)
    font = pygame.font.SysFont(None, size)
    font.set_bold(True)
    if outline > 0:
        outlineSurf = font.render(text, True, (255, 0, 0))
        outlineSize = outlineSurf.get_size()
        textSurf = pygame.Surface((outlineSize[0] + outline*2, outlineSize[1] + 2*outline))
        textRect = textSurf.get_rect()
        offsets = [(ox, oy) 
            for ox in range(-outline, 2*outline, outline)
            for oy in range(-outline, 2*outline, outline)
            if ox != 0 or ox != 0]
        for ox, oy in offsets:   
            px, py = textRect.center
            textSurf.blit(outlineSurf, outlineSurf.get_rect(center = (px+ox, py+oy))) 
        innerText = font.render(text, True, color).convert_alpha()
        textSurf.blit(innerText, innerText.get_rect(center = textRect.center)) 
    else:
        textSurf = font.render(text, True, color)
        textRect = textSurf.get_rect()    

    textRect.center = (x,y)
    screen.blit(textSurf, textRect)

run = True
while run:
    clock.tick(60)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
    hover = textRect.collidepoint(pygame.mouse.get_pos())
    outlineSize = 3 if hover else 0 

    screen.fill((0,0,0))
    text_speech('arialnarrow.ttf', 40, 'Hello', (255,255,255), (width/2), (height/2), False, outlineSize)
    pygame.display.flip()

Assuming that by "outline" you mean a stroke around it, I've got and easy solution. Simply render the same text, centered around the same position as the text you've already written, a bit bigger and in red. Then, just check when the mouse is hovering over the rect of your initial text, and if so, blit the outline.

In order to do this, we need to extract the rect of your first text. I changed your function so that it outputs the rendered surface, and rect.

I also made a few other adjustments :

  • You don't need to generate the font and render the text each time, this wastes CPU cycles. I recommend setting each of your fonts as global constants, for each size/typeface
  • You define a screen within your function, but never use it. I changed the function so that it no longer does the job of rendering.
  • When you call text_speech (I assume your second usage is a typo), width and height don't refer to anything. I also defined them as global constants, which I set to be your display size.

You haven't included any display code, so I wrote the bare minimum for a running concept.

import pygame

pygame.init()

# Font constants
ARIALNARROW_40 = font = pygame.font.Font('arialnarrow.ttf', 40)
ARIALNARROW_42 = font = pygame.font.Font('arialnarrow.ttf', 42)

# Screen size
WIDTH = 900
HEIGHT = 600

def text_speech(font, text, color, x, y, bold):
    font.set_bold(bold)
    rendered_text = font.render(text, True, color)

    # Directly center the rect upon its creation
    text_rect = rendered_text.get_rect(center=(x,y))
    return text_rect, rendered_text

screen = pygame.display.set_mode((WIDTH, HEIGHT))

inner_rect, inner_text = text_speech(
    ARIALNARROW_40, 'Hello', (255, 255, 255),
    (WIDTH / 2), (HEIGHT / 2), False
)

# For your outline
outline_rect, outline_text = text_speech(
    ARIALNARROW_42, 'Hello', (255, 0, 0),
    (WIDTH / 2), (HEIGHT / 2), False
)

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            exit()


    # Paint our screen
    screen.fill((0,0,0))

    if inner_rect.collidepoint(pygame.mouse.get_pos()):
        # Touching our text! Render outline
        screen.blit(outline_text, outline_rect)

    screen.blit(inner_text, inner_rect)


    # Enact our display changes
    pygame.display.update()

Note This does add the potentially unwanted affected of having a "side zoom". However getting around this would mean that'd you either have to mess around with font kerning (with the pygame.freetype module) but that could get very messy very fast, or you could prerender a stroke image ahead of time (and blit it using the same logic I used) but that would require you to rerender every time you changed the text, for all your text surfaces.

Related