DEV Community

Choon-Siang Lai
Choon-Siang Lai

Posted on • Originally published at python.plainenglish.io on

Recreating the Matrix Rain with Pygame: Manual Fades and the Transparency of Code.

Earlier in the year, we explored Pygame by writing a simple tic-tac-toe game. The exercise unexpectedly yielded a reusable mini framework. Despite being far from “production-ready”, it is still sufficiently good for smaller-scale experiments. One of the possible projects mentioned towards the end of the article was a remake of the video intro with the Matrix code rain effect. Even though I phrased it as a joke at the time, we are diving into it in this article.


A cute cartoon generated by my editor Gemini

Returning to the Source: A Baby-Steps Approach

We are taking a baby-steps approach in this remake project, as I am rather tied up with my full-time work commitments. In this installment, we are only focusing on the fade-out animation and will continue working towards a full recreation in the future. The fade-out effect in the animation was made possible through the use of jQuery’s .fadeOut() method, which fades an element out in a specified duration.

How do we do it in jQuery, one may wonder? The snippet below shows how it is being done in the context of a web application.

<!-- html -->
<p id="target">Lorem ipsum dolor sit amet</p>

<script>
  // jQuery
  $("#target").fadeOut()
</script>
Enter fullscreen mode Exit fullscreen mode

Deconstructing the Magic: Recreating .fadeOut()

Now that we know how it’s done with jQuery, the logical next step is to start defining the problem. Revisions to the framework may be required if there’s a need to support the specific effect. Actual implementation comes a bit later, after drafting a proper plan. Read on to follow along.

What is the problem we are solving today?

We want to make it possible to render a character on the screen, and repeatedly re-render the character with a different alpha value until it reaches 0. Each character is expected to be rendered over a pygame.Rect and we are assuming there are no overlaps in our application. Additionally, the alpha value decreases linearly over the specified duration, which should keep the implementation simple.


a screen recording of the desired outcome

Next, we figure out what is needed from the framework. In order to trigger a re-render at a consistent interval, we need to post an event in our rendering loop. Let’s call it FRAME_NEXT, and add it to the enum of SystemEvent alongside the INIT event we saw in the tic-tac-toe project.

That yields an updated display_update function, as shown below:

async def display_update(
    screen_update: asyncio.Queue, clock: pygame.time.Clock
) -> None:
    clock.tick(60)

    updates = []

    with suppress(asyncio.queues.QueueEmpty):
        while element := screen_update.get_nowait():
            updates.append(element.position)

    pygame.display.update(updates)

    # Trigger new FRAME_NEXT event
    pygame.event.post(pygame.event.Event(SystemEvent.FRAME_NEXT.value))
Enter fullscreen mode Exit fullscreen mode

Printing a character to the screen is similar to how we draw a circle or a cross in the tic-tac-toe project. First, we draw a pygame.Rect, then we draw the rendered text surface object over it. Start by rendering the character, and we get the corresponding text_surface and the rect enclosing it:

import pygame.freetype

font = pygame.freetype.Font("/path/to/font.ttf", font_size)

text_surface, rect = font.render('a', (red, green, blue, alpha))
Enter fullscreen mode Exit fullscreen mode

The self-explanatory font_size, red, green, blue, alpha are all integers, and the latter four are in the range of [0, 255].

Given both the text_surface and enclosing rect, we can print them to the screen at any desired location as follows:

# optionally we can move the rect
rect.topleft = (x, y)

# first draw the backing rect
pygame.draw.rect(application.screen, (0, 0, 0), rect)

# secondly draw the rendered character
application.screen.blit(text_surface, rect)
Enter fullscreen mode Exit fullscreen mode

We can finally assemble two functions, one to create an initial print, and another to update the region with the same character but with a different alpha value:

@dataclass(frozen=True)
class CharElem(Element):
    value: str = " "
    font: pygame.freetype.Font | None = None
    color: tuple[int, int, int] = (0, 0, 0)
    alpha: int = 0

async def char_create(
    application: Application,
    value: str,
    size: int,
    color: tuple[int, int, int],
    alpha: int,
    position: tuple[int, int],
) -> CharElem:
    assert len(value) == 1 and isinstance(value, str)

    font = pygame.freetype.Font("/path/to/font.ttf", size)

    surface, rect = font.render(value, (255, 255, 255, alpha))

    rect.topleft = position

    pygame.draw.rect(application.screen, (0, 0, 0), rect)
    application.screen.blit(surface, rect)

    return CharElem(position=rect, value=value, font=font, color=color, alpha=alpha)

async def char_update(application: Application, elem: CharElem, alpha: int) -> CharElem:
    assert elem.position
    assert elem.font

    surface, _ = elem.font.render(elem.value, elem.color + (alpha,))

    pygame.draw.rect(application.screen, (0, 0, 0), elem.position)
    application.screen.blit(surface, elem.position)

    return replace(elem, alpha=alpha)
Enter fullscreen mode Exit fullscreen mode

Updating a drawn character returned by char_create is done through a handler listening to the FRAME_NEXT event we defined earlier. An extension to the usual element event handler is hence needed to hold two extra pieces of data: the duration and start time. Fortunately, this is trivial with the use of __call__ dunder:

@dataclass
class FadeOutHandler:
    duration: int
    start_time: int = field(default_factory=lambda: monotonic_ns() // 1_000_000)

    async def __call__ (
        self,
        event: pygame.event.Event,
        target: CharElem,
        application: Application,
        logger: BoundLogger,
        **detail: Any,
    ) -> None:
        current = monotonic_ns() // 1_000_000
        elapsed = current - self.start_time
        remaining = 1.0 - (elapsed / self.duration)
        alpha = int(max(255 * remaining, 0))

        elem = await char_update(application, target, alpha)

        asyncio.create_task(screen_update(application, elem))

        if alpha > 0:
            asyncio.create_task(
                application.delta_data.put(
                    DeltaUpdate(ApplicationDataField.ELEMENTS, target, elem)
                )
            )
        else:
            asyncio.create_task(
                application.delta_data.put(
                    DeltaDelete(ApplicationDataField.ELEMENTS, target)
                )
            )
Enter fullscreen mode Exit fullscreen mode

Some points worth noting regarding the snippet:

  1. As with the .fadeOut(duration) offered by jQuery, we define the duration in milliseconds
  2. We remain somewhat stateless by storing just the start_time, and we calculate the alpha value at any time by comparing it to the percentage of remaining time.
  3. After alpha reaches 0, remember to remove the element reference from the application

The Versatility of __call__: A Python Developer's Secret Weapon

Registering the event can then be done through a helper function:

async def fade_out(application: Application, elem: CharElem, duration: int) -> CharElem:
    return await add_event_listener(
        elem,
        SystemEvent.FRAME_NEXT.value,
        FadeOutHandler(duration),
    )
Enter fullscreen mode Exit fullscreen mode

Are we done yet?

One last piece of the puzzle is the click event handler, where a character is shown and eventually fades out into oblivion wherever the user clicks on the screen.

async def handle_click(
    event: pygame.event.Event, target: Application, logger: BoundLogger
) -> None:
    elem = await fade_out(
        target,
        await char_create(target, "a", 20, (255, 255, 0), 255, event.pos),
        1000,
    )

    asyncio.create_task(screen_update(target, elem))
    asyncio.create_task(
        target.delta_data.put(DeltaAdd(ApplicationDataField.ELEMENTS, elem))
    )
Enter fullscreen mode Exit fullscreen mode

We are almost done, though I am skipping the boilerplate bootstrapping code as it is rather similar to the tic-tac-toe application. Basically, the code sets up the window, paints the background colour, and registers the click event. For those interested, do check the GitHub repository for the complete application.

GitHub - Jeffrey04/kitfucoda-intro2

The Cracks in the Framework: Scalability and Loops

Some expansion to the framework was already done when I was building the tic-tac-toe game earlier. Apart from some small syntactic changes made to the API, I was pleasantly surprised to find the overall framework design was left untouched. That’s good news, no?

A revision to the DeltaOperation was done previously to consolidate all changes to the Application object into one queue. It was a sensible improvement to the original implementation, but I still don’t quite like the complexity of the queue consumption. Sooner or later scalability will become an issue we want to address when the need to draw hundreds of elements at a time arises.

Furthermore, another pain point occurs in the event dispatching loop. Ideally, we want a quick lookup that is close to O(1). Unfortunately, we are looping through elements followed by events on every cycle. Performance is definitely taking a hit as we are running everything concurrently and we are wasting so much time looping needlessly in lookup operations.

What’s next then? I suppose you may wonder where we are headed, if you are still following thus far.

Apparently the refactoring that was done to the DeltaOperation somewhat resembles the Actor model. Further research and experiments are needed to see how it fits the design. From what I know so far, the model offers a cleaner separation of data storage and retrieval logic from the rest of the application. Hopefully that possibility of clean separation allows some independence. This can be helpful as it enables us to further split different parts of the application to run in parallel. This would be challenging, given how Pygame is not even thread-safe.

That means the next installment should focus on the optimization instead of continuing the momentum of building the next feature — the chain of characters raining down the screen.

Visibility, Implementation, and Identity

Just to recap, we achieved the goal of building an almost equivalent to jQuery’s .fadeOut() effect that works with our mini Pygame framework. An exact copy is never the goal, considering the rendering technique is different and we are synchronizing the change with the frame rate. Implementing the gradual decrease of alpha value really does take some effort in designing and planning. This is truly an interesting learning journey and I hope you benefit from following along.

It really does make you appreciate all the work done to provide the functionality through a simple method call.

The reimplementation in this low-level setting leads to a personal reflection related to recent news that deeply saddens me. There seem to be ongoing raid operations targeting men-only wellness centres. In one of those operations, a video was leaked across social media where the arrested patrons could be seen unclothed. A list with enough identifying information sans the name was also circulated.

Every line of code written in the low-level setting is very explicit and corresponds to a visible consequence. This kind of transparency prompted a revelation where, as a gay man, it is crucial to increase the visibility of members of the sexual minority. There are a lot of us in society working earnestly and also actively taking part in our civil duties. It is important to recognize that everyone in society is multi-faceted. As long as no harm is caused, one’s private life should remain private. Judging a person solely on visiting a wellness centre is simply unfair. Even the authorities fail to bring the patrons to court due to lack of evidence in one of the raids. Unfortunately, some still faced repercussions at work, especially the public servants.

Hopefully you gain insights while reading the articles published here on KitFu Coda, which is my ultimate goal as a fellow engineer. My sexuality indeed doesn’t matter to the content, but it is the best way I could think of to voice my support and solidarity with those who are still suffering the consequences to this day.

Lastly, thank you for reading, and I shall write again soon.

This article was written with editorial assistance from Gemini to refine the language and structure. Please note that while the AI helped catch errors, all project ideas, code algorithms, and personal perspectives are my own original work.


Top comments (0)