Search code examples
pythonpygametextcolortext-formatting

how to change the color a section of text in pygame


I am creating a game with pygame in which the color of a letter changes when you type that letter. Like nitrotype.com. However the problem is that I don't know how to change the colour of individual letters. I can't clear the screen and then do it because then that would change the color of the entire line. So either I need a way to change the colour of individual letters or a way to just put single letters on the screen one at a time. However I don't know how to uniformly put the letters(such that the end sentence is centered). Please could someone help me out here. Either by telling me how to change the color of individual letters or how to put individual letters in a perfect manner and then change their color.

import pygame as pg
import pygame

pg.init()
screenHeight, screenWidth = 600, 800
gameDisplay = pg.display.set_mode((screenWidth, screenHeight))
pg.display.set_caption("Nitrotype")

black = (255, 255, 255)
white = (0, 0, 0)
gameDisplay.fill(white)
pg.display.update()

gameOn = True
with open("text.txt", "r") as f:
    contents = f.read()

def msgToScreen(msg, color, size):
    cur = []
    strings = []
    words = msg.split(" ")
    for i in words:
        cur.append(i)
        if len(" ".join(cur)) >= 35:
            strings.append(" ".join(cur))
            cur = []
    if cur != []:strings.append(" ".join(cur))
    
    curY = 20
    
    for string in strings:
        font = pg.font.SysFont(None, size)
        text = font.render(string, True, color)
        text_rect = text.get_rect(center=(screenWidth/2, curY))
        gameDisplay.blit(text, text_rect)
        curY += 40
    
    return text

textOnScreen = msgToScreen(contents, black, 50)

pg.display.update()

curIdx = 0
keyCombination = {"a":pg.K_a, "b":pg.K_b, "c":pg.K_c, "d":pg.K_d, "e":pg.K_e, "f":pg.K_f,
                "g":pg.K_g, "h":pg.K_h, "i":pg.K_i, "j":pg.K_j, "k":pg.K_k, "l":pg.K_l,
                "m":pg.K_m, "n":pg.K_n, "o":pg.K_o, "p":pg.K_p, "q":pg.K_q, "r":pg.K_r,
                "s":pg.K_s, "t":pg.K_t, "u":pg.K_u, "v":pg.K_v, "w":pg.K_w, "x":pg.K_x,
                "y":pg.K_y, "z":pg.K_z}
while gameOn:
    for event in pygame.event.get():
        if event.type == pg.QUIT:
            gameOn = False
        if event.type == pg.KEYDOWN:
            if event.key == keyCombination[contents[curIdx].lower()]:
                #Here is where the color of the current letter should change
                curIdx += 1

pg.quit()


Solution

  • You can't change the color of a single letter during font rendering; you'll have to render your text letter by letter.

    You can either use render() to render each letter to its own surface and blit them to your screen, but you have to calculate where each letter should go manually.

    It's a little bit easier if you use the new freetype module, which has a lot of handy functions in the Font class like origin, get_rect and get_metrics which can calculate how big each letter is.

    Here's a simple example I hacked together. It's not perfect but you'll get the idea.

    import pygame
    import pygame.freetype
    from itertools import cycle
    
    def main():
        pygame.init()
        screen = pygame.display.set_mode((800, 600))
    
        # just some demo data for you to type
        data = cycle(['This is an example.', 'This is another, longer sentence.'])
        current = next(data)
        current_idx = 0 # points to the current letter, as you have already guessed
        
        font = pygame.freetype.Font(None, 50)
        # the font in the new freetype module have an origin property.
        # if you set this to True, the render functions take the dest position 
        # to be that of the text origin, as opposed to the top-left corner
        # of the bounding box
        font.origin = True
        font_height = font.get_sized_height()
        
        # we want to know how much space each letter takes during rendering.
        # the item at index 4 is the 'horizontal_advance_x'
        M_ADV_X = 4
        
        # let's calculate how big the entire line of text is
        text_surf_rect = font.get_rect(current)
        # in this rect, the y property is the baseline
        # we use since we use the origin mode
        baseline = text_surf_rect.y
        # now let's create a surface to render the text on
        # and center it on the screen
        text_surf = pygame.Surface(text_surf_rect.size)
        text_surf_rect.center = screen.get_rect().center
        # calculate the width (and other stuff) for each letter of the text
        metrics = font.get_metrics(current)
    
        while True:
            events = pygame.event.get()
            for e in events:
                if e.type == pygame.QUIT:
                    return
                if e.type == pygame.KEYDOWN:
                    if e.unicode == current[current_idx].lower():
                        # if we press the correct letter, move the index
                        current_idx += 1
                        if current_idx >= len(current):
                            # if the sentence is complete, let's prepare the
                            # next surface
                            current_idx = 0
                            current = next(data)
                            text_surf_rect = font.get_rect(current)
                            baseline = text_surf_rect.y
                            text_surf = pygame.Surface(text_surf_rect.size)
                            text_surf_rect.center = screen.get_rect().center
                            metrics = font.get_metrics(current)
    
            # clear everything                        
            screen.fill('white')
            text_surf.fill('white')
            
            x = 0
            # render each letter of the current sentence one by one
            for (idx, (letter, metric)) in enumerate(zip(current, metrics)):
                # select the right color
                if idx == current_idx:
                    color = 'lightblue'
                elif idx < current_idx:
                    color = 'lightgrey'
                else:
                    color = 'black'
                # render the single letter
                font.render_to(text_surf, (x, baseline), letter, color)
                # and move the start position
                x += metric[M_ADV_X]
              
            screen.blit(text_surf, text_surf_rect)
            pygame.display.flip()
    
    if __name__ == '__main__':
        main()
    

    Centering the text is easy using a second Surface and using the Rect class' center property.

    enter image description here