2

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?

Rabbid76
  • 202,892
  • 27
  • 131
  • 174
walid
  • 77
  • 10

2 Answers2

3

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()
Rabbid76
  • 202,892
  • 27
  • 131
  • 174
  • 1
    Honestly, both of you are right, however you just make a outline around the whole text. Thanks a lot! – walid Apr 02 '20 at 09:46
  • @underscoreC Thank you. If you want to use the pygame.font module, then this is the only option. Of course the pygame.freetype module would give a larger scale of opportunities. – Rabbid76 Apr 02 '20 at 09:54
  • @Rabbid76 hey, sorry for inserting myself just like this, but do you mind explaining why you used `.convert_alpha()` in there? I more or less understand WHAT the command does, but I just don't understand WHY would anyone ever use that. And if it has anything to do with performance, is it worth using it for blitting the outline? because blitting 24 text objects every iteration is making my fans bleed. (I have 3 places where I need to have the outline constantly) – hellwraiz Jun 05 '21 at 21:20
  • @hellwraiz [`convert()`](https://www.pygame.org/docs/ref/surface.html#pygame.Surface.convert) or [`convert_alpha()`](https://www.pygame.org/docs/ref/surface.html#pygame.Surface.convert_alpha)) ensures that the a _Surface_ has the same pixel format as the display _Surface_. This improves performance when the image is `blit` on the display, because the formats are compatible and `blit` does not need to perform an implicit transformation. See [Lag when win.blit() background pygame](https://stackoverflow.com/questions/59312019/lag-when-win-blit-background-pygame/59318946#59318946) – Rabbid76 Jun 05 '21 at 21:21
2

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.

underscoreC
  • 729
  • 6
  • 13