0

I want to copy the effect shown in the Watson-scott test, where the text seems to glow. Link to the example: https://www.youtube.com/watch?v=2ySNm4gltkE

Skip to 11:17 where the text seems to glow; how do I replicate that effect with pygame? I tried adding a greyish rectangle in the background of the text but it just looks awful. I also tried shadowing the text like this example but nothing works.

Also I'm using Python 3.7.4. Thanks for any help I really need it!!

Rebecca
  • 23
  • 1
  • 6
  • PyGame is not the right choice for this. – Rabbid76 May 16 '21 at 20:11
  • @Rabbid76 Then what should I do? I only know Python... – Rebecca May 16 '21 at 20:12
  • Of course it can be done with Python, but it is hardly possible with PyGame. Such an effect is usually achieved with a GPU-bound approach using shaders. – Rabbid76 May 16 '21 at 20:14
  • The closest you can get with PyGame is something like: [Have an outline of text in Pygame](https://stackoverflow.com/questions/60987711/have-an-outline-of-text-in-pygame/60988595#60988595) – Rabbid76 May 16 '21 at 20:16
  • [Python Pygame Lighting for Pong](https://stackoverflow.com/q/65609962/1324033) - also related – Sayse May 16 '21 at 20:17
  • I don't know if this can be done there but [`ursina engine`](https://www.ursinaengine.org/) is a python game engine (that can also be used to create 3D games easily) but maybe it has something that could make text "glow". – Matiiss May 16 '21 at 20:46
  • This guy has clouds in his game which look similar to the effect you want: https://old.reddit.com/r/pygame/comments/ndxt5c/adding_some_foreground_effects_clouds_to_keep_it/ – marienbad May 17 '21 at 08:39
  • @marienbad This are bitmaps – Rabbid76 May 17 '21 at 08:43

1 Answers1

11

Well sometimes we can say it is not possible, but often times it is just not the main goal of that package. Nonetheless, let's see if we can solve the problem.

I am taking the liberty of assuming that other packages besides pygame are allowed, but that the end result should be visible in pygame. In order to create the blooming / glowing effect I use the packages opencv-python (cv2) and numpy (np).

The first part of the solution will talk about creating a glowing border and some glowing text. The second part will talk about how this can be rendered upon a pygame surface.

TL;DR; Skip to the Summary part below and copy the code in their respective files.

Part 1

Blooming

In order to get some nice glowing borders and text, we can use the blurring functionality of opencv, which is also called smoothing. Since we want to create varying intensity of glowing, we first apply the GaussianBlur, to create some random blurriness around the image, and then extend that blurriness with the normal blur.

def apply_blooming(image: np.ndarray) -> np.ndarray:
    # Provide some blurring to image, to create some bloom.
    cv2.GaussianBlur(image, ksize=(9, 9), sigmaX=10, sigmaY=10, dst=image)
    cv2.blur(image, ksize=(5, 5), dst=image)
    return image

Note: the values for the kernel sizes (ksize), and the sigmas (sigmaX and sigmaY) have been chosen empirically, you play a bit around with those values, until you get what you want.

Colors

A small intermezzo, since we will need to provide some very nice scary coloring, the following class holds some (scary) colors.

class Colors:
    WHITE_ISH = (246, 246, 246)
    YELLOW_ISH = (214, 198, 136)
    RED_ISH = (156, 60, 60)

Glowing border

In order to get a glowing border, a helper function was made, that will draw a rectangle with some predefined properties. The chosen properties are:

  • margin: The border will be drawn that far away from the image sides.
  • thickness: The border will be made of this many pixels.
  • color: The color of the border, such that it can be easily changed.
def create_border(image: np.ndarray, margin: int, thickness: int, color: Colors) -> np.ndarray:
    """
    Create a normal border around an image, with specified colors.

    Args:
        image: The image, that requires a border.
        margin: The border distance from the sides of the image.
        thickness: The thickness of the border.
        color: The border color, by default a slightly yellow color.

    Modifies:
        The input image, will be modified with a border.

    Returns:
        The same image, with a border inserted.

    """

    # Numpy uses the convention `rows, columns`, instead of `x, y`.
    # Therefore height, has to be before width.
    height, width = image.shape[:2]
    cv2.rectangle(image, (margin, margin), (width - margin, height - margin), color, thickness=thickness)
    return image

The final border can then be drawn using the apply_blooming and create_border functions.

def glowing_border(image: np.ndarray, margin=20, thickness=10, color: Colors = Colors.WHITE_ISH):
    """

    Create a glowing border around an image.

    Args:
        image: The image, that requires a border.
        margin: The border distance from the sides of the image.
        thickness: The thickness of the border.
        color: The border color, by default a slightly yellow color.

    Modifies:
        The input image, will be modified with a blooming border.

    Returns:
        The same image, with a blooming border inserted.
    """

    # Generate yellowish colored box
    image = create_border(image, margin, thickness, color)

    # Apply the blooming.
    image = apply_blooming(image)

    # Reassert the original border, to get a clear outline.
    # Similar to the Watson-Scott test, two borders were added here.
    image = create_border(image, margin - 1, 1, color)
    image = create_border(image, margin + 1, 1, color)
    return image

Testing code

In order to test the glowing border, we can use cv2.imshow, to display the image. Since we are going to use this functionality later on, a small function was created. This function will take as input the image, and a displaying time (waiting time before code execution continues).


def show(image, delay=0):
    """
    Display an image using cv2.

    Notes:
        By default cv2 uses the BGR coloring, instead RGB.
        Hence image shown by cv2, which are meant to be RGB,
        has to be transformed using `cvtColor`.

    Args:
        image: Input image to be displayed
        delay: Time delay before continuing running.
            When 0, The program will wait until a key stroke or window is closed.
            When 1, The program will continue as quickly as possible.

    Returns:
        Nothing, it displays the image.

    """
    cv2.imshow('Test', cv2.cvtColor(image, cv2.COLOR_RGB2BGR))
    cv2.waitKey(delay)

Actual test code:

image = np.zeros((480, 640, 3), dtype=np.uint8)
border = glowing_border(image.copy(), color=Colors.YELLOW_ISH)
show(border, delay=0)

Glowing text

A similar approach can be used for the glowing text, by using cv2.putText.

def glowing_text(image: np.ndarray, text: str, org: Tuple[int, int], color: Colors) -> np.ndarray:
    """

    Args:
        image: The image, that requires a border.
        text: The text to be placed on the image.
        org: The starting location of the text.
        color: The color of the text.


    Modifies:
        The input image, will be modified with a blooming text.

    Returns:
        The same image, with a blooming text inserted.
    """

    image = cv2.putText(image, text, org, cv2.FONT_HERSHEY_COMPLEX_SMALL, fontScale=.7, color=color, thickness=1)
    image = apply_blooming(image)
    image = cv2.putText(image, text, org, cv2.FONT_HERSHEY_COMPLEX_SMALL, fontScale=.7, color=color, thickness=1)
    return image

With test code

image = np.zeros((480, 640, 3), dtype=np.uint8)
text = glowing_text(image.copy(), text="Welcome to this game", org=(50, 70), color=Colors.YELLOW_ISH)
show(text, delay=0)

Intermezzo

Before I go on and show how this can be displayed in pygame, I will throw in a bonus and show how the text can appear on the screen, as if a human was typing it in slowly. The reason that the following code works, is because we separately draw the border and the text, and then combine the results using the np.bitwise_or.

image = np.zeros((480, 640, 3), dtype=np.uint8)

# Create the glowing border, and a copy of the image, for the text, that will be placed on it later.
border = glowing_border(image.copy(), color=Colors.YELLOW_ISH)
text = image.copy()

# This message will be incrementally written
message = "Welcome to this game. Don't be scared :)."

for idx in range(len(message) + 1):
    text = glowing_text(image.copy(), text=message[:idx], org=(50, 70), color=Colors.YELLOW_ISH)

    # We use a random time delay between keystrokes, to simulate a human.
    show(np.bitwise_or(border, text), delay=np.random.randint(1, 250))

# Pause the screen after the full message.
show(np.bitwise_or(border, text), delay=0)

Note: Alternatively we could first generate the border and text on the same image, and then apply the blooming filter. Just keep in mind that we then have to redraw the border and text again, to give them a solid basis.

Part 2

Now that we can generate a canvas with the right blooming border and text, it has to be inserted into pygame. Let's put all the previous functions into a file called blooming.py, and reference it in the new file game.py.

The following code is a minimal working example of how to put a numpy array into pygame.

import contextlib
from typing import Tuple

# This suppresses the `Hello from pygame` message.
with contextlib.redirect_stdout(None):
    import pygame

import numpy as np
import blooming


def image_generator(size: Tuple[int, int], color: blooming.Colors):
    image = np.zeros((*size[::-1], 3), dtype=np.uint8)

    # Create the glowing border, and a copy of the image, for the text, that will be placed on it later.
    border = blooming.glowing_border(image.copy(), color=color)
    text = image.copy()

    # This message will be incrementally written
    message = "Welcome to this game. Don't be scared :)."

    for idx in range(len(message) + 1):
        text = blooming.glowing_text(image.copy(), text=message[:idx], org=(50, 70), color=color)
        yield np.bitwise_or(border, text)
    return np.bitwise_or(border, text)


if __name__ == '__main__':
    pygame.init()
    screen = pygame.display.set_mode((640, 480))
    clock = pygame.time.Clock()
    running = True

    while running:
        for image in image_generator(screen.get_size(), color=blooming.Colors.YELLOW_ISH):
            screen.fill((0, 0, 0))

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False

                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        running = False

            # This is where we insert the numpy array.
            # Because pygame and numpy use different coordinate systems,
            # the numpy image has to be flipped and rotated, before being blit.
            img = pygame.surfarray.make_surface(np.fliplr(np.rot90(image, k=-1)))
            screen.blit(img, (0, 0))

            pygame.display.flip()
            clock.tick(np.random.randint(10, 30))

    pygame.quit()

Summary (TL;DR;)

  • blooming.py
from typing import Tuple

import cv2
import numpy as np


class Colors:
    WHITE_ISH = (246, 246, 246)
    YELLOW_ISH = (214, 198, 136)
    RED_ISH = (156, 60, 60)


def create_border(image: np.ndarray, margin: int, thickness: int, color: Colors) -> np.ndarray:
    """
    Create a normal border around an image, with specified colors.

    Args:
        image: The image, that requires a border.
        margin: The border distance from the sides of the image.
        thickness: The thickness of the border.
        color: The border color, by default a slightly yellow color.

    Modifies:
        The input image, will be modified with a border.

    Returns:
        The same image, with a border inserted.

    """

    # Numpy uses the convention `rows, columns`, instead of `x, y`.
    # Therefore height, has to be before width.
    height, width = image.shape[:2]
    cv2.rectangle(image, (margin, margin), (width - margin, height - margin), color, thickness=thickness)
    return image


def apply_blooming(image: np.ndarray) -> np.ndarray:
    # Provide some blurring to image, to create some bloom.
    cv2.GaussianBlur(image, ksize=(9, 9), sigmaX=10, sigmaY=10, dst=image)
    cv2.blur(image, ksize=(5, 5), dst=image)
    return image


def glowing_border(image: np.ndarray, margin=20, thickness=10, color: Colors = Colors.WHITE_ISH):
    """

    Create a glowing border around an image.

    Args:
        image: The image, that requires a border.
        margin: The border distance from the sides of the image.
        thickness: The thickness of the border.
        color: The border color, by default a slightly yellow color.

    Modifies:
        The input image, will be modified with a blooming border.

    Returns:
        The same image, with a blooming border inserted.
    """

    # Generate yellowish colored box
    image = create_border(image, margin, thickness, color)

    # Apply the blooming.
    image = apply_blooming(image)

    # Reassert the original border, to get a clear outline.
    # Similar to the Watson-Scott test, two borders were added here.
    image = create_border(image, margin - 1, 1, color)
    image = create_border(image, margin + 1, 1, color)
    return image


def glowing_text(image: np.ndarray, text: str, org: Tuple[int, int], color: Colors) -> np.ndarray:
    """

    Args:
        image: The image, that requires a border.
        text: The text to be placed on the image.
        org: The starting location of the text.
        color: The color of the text.


    Modifies:
        The input image, will be modified with a blooming text.

    Returns:
        The same image, with a blooming text inserted.
    """

    image = cv2.putText(image, text, org, cv2.FONT_HERSHEY_COMPLEX_SMALL, fontScale=.7, color=color, thickness=1)
    image = apply_blooming(image)
    image = cv2.putText(image, text, org, cv2.FONT_HERSHEY_COMPLEX_SMALL, fontScale=.7, color=color, thickness=1)
    return image


def show(image, delay=0):
    """
    Display an image using cv2.

    Notes:
        By default cv2 uses the BGR coloring, instead RGB.
        Hence image shown by cv2, which are meant to be RGB,
        has to be transformed using `cvtColor`.

    Args:
        image: Input image to be displayed
        delay: Time delay before continuing running.
            When 0, The program will wait until a key stroke or window is closed.
            When 1, The program will continue as quickly as possible.

    Returns:
        Nothing, it displays the image.

    """
    cv2.imshow('Test', cv2.cvtColor(image, cv2.COLOR_RGB2BGR))
    cv2.waitKey(delay)


if __name__ == '__main__':
    image = np.zeros((480, 640, 3), dtype=np.uint8)

    # Create the glowing border, and a copy of the image, for the text, that will be placed on it later.
    border = glowing_border(image.copy(), color=Colors.YELLOW_ISH)
    text = image.copy()

    # This message will be incrementally written
    message = "Welcome to this game. Don't be scared :)." + " " * 10

    for idx in range(len(message) + 1):
        text = glowing_text(image.copy(), text=message[:idx], org=(50, 70), color=Colors.YELLOW_ISH)

        # We use a random time delay between keystrokes, to simulate a human.
        show(np.bitwise_or(border, text), delay=np.random.randint(1, 250))

    # Pause the screen after the full message.
    show(np.bitwise_or(border, text), delay=0)
  • game.py
import contextlib
from typing import Tuple

# This suppresses the `Hello from pygame` message.
with contextlib.redirect_stdout(None):
    import pygame

import numpy as np
import blooming


def image_generator(size: Tuple[int, int], color: blooming.Colors):
    image = np.zeros((*size[::-1], 3), dtype=np.uint8)

    # Create the glowing border, and a copy of the image, for the text, that will be placed on it later.
    border = blooming.glowing_border(image.copy(), color=color)
    text = image.copy()

    # This message will be incrementally written
    message = "Welcome to this game. Don't be scared :)." + " " * 10

    for idx in range(len(message) + 1):
        text = blooming.glowing_text(image.copy(), text=message[:idx], org=(50, 70), color=color)
        yield np.bitwise_or(border, text)
    return np.bitwise_or(border, text)


if __name__ == '__main__':
    pygame.init()
    screen = pygame.display.set_mode((640, 480))
    clock = pygame.time.Clock()
    running = True

    while running:
        for image in image_generator(screen.get_size(), color=blooming.Colors.YELLOW_ISH):
            screen.fill((0, 0, 0))

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False

                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        running = False

            # This is where we insert the numpy array.
            # Because pygame and numpy use different coordinate systems,
            # the numpy image has to be flipped and rotated, before being blit.
            img = pygame.surfarray.make_surface(np.fliplr(np.rot90(image, k=-1)))
            screen.blit(img, (0, 0))

            pygame.display.flip()
            clock.tick(np.random.randint(10, 30))

    pygame.quit()

Result

Please note that the real thing looks a lot sharper than this image. Also playing around with the thickness of the text and the sizes of the blurring filters will influence the result quite a bit. For this image, the ksize of the GaussianBlur has been increased to (17, 17), and the sigmaX and sigmaY have both been put to 100.

enter image description here

Thymen
  • 2,089
  • 1
  • 9
  • 13
  • 1
    Isn't `cv` complete overkill? Doesn't PIL provide something similar? – MegaIng May 16 '21 at 22:44
  • 1
    I think that there are many ways to obtain some kind of blurring / smoothing or enhancing. For me `opencv` is the most familiar one, but no doubt `PIL`, `scipy` or even standalone `numpy` can do this. – Thymen May 16 '21 at 22:56
  • Hey thank you so much for doing this and helping me and writing that whole answer out. This is literally so cool thanks for taking the time:) – Rebecca May 18 '21 at 00:05
  • No problem, you are welcome ^^. A small addition, the above is al written in RGB coding. If you want to apply this to an already existing image, you can also use RGBA, encoding. That adds an alpha layer that controls the visibility, where 0 means it is hidden and 255 means it is shown. In that case, you can apply this method on Pygame while maintaining other features. – Thymen May 18 '21 at 07:54