8.3.1 - Star Wars Titles#

Have you ever wondered how you could produce an animation like in the Star Wars main titles?

Of course this can be done in Python:

image0

How it works:#

The text uses the “Zen of Python” (obtained via import this and adding a few line breaks). First, a plain image of the text is created. In the script, OpenCV is used, but you can do the same with any image of your choice, as long as the image is sufficiently long.

image1

The main challenge is to calculate the perspective. For every pixel (x, y) on the screen, you want to know what is the closest pixel (x0, y0) on the text image. The equations to calculate those are:

\[x0 = x - \frac{x \cdot y}{y-1}\]
\[y0 = - \frac{y \cdot c}{y-1}\]

where

  • y is a float in the range of 0..1.

  • x, x0 and y0 are absolute pixel positions

  • c is a constant for the distance of the observer. It defines how “steep” the trapezoid will be

In practice you need to do some scaling of these numbers to center the animation on the screen.

There is probably a more efficient way to calculate the perspective (using more linear algebra). But I found this solution from scratch on a piece of paper and made it work fast enough.

Installation#

The script requires the OpenCV library for displaying the live animation. A second script in the repository uses imageio for exporting animated GIFs.

pip install opencv-python
pip install imageio

The code#

import numpy as np
import numpy as np
import cv2
import time
import imageio


XSIZE = 1400       # width of the screen
YSIZE = 800
TEXT_YSIZE = 3000  # length of the bitmap that is scrolled through

YELLOW = (0, 255, 255)
WHITE = (255, 255, 255)


def create_text_bitmap(fn, color=YELLOW, line_spacing=50):
    """
    returns a numpy array that contains the entire text from the file
    """
    text = open('message.txt')
    msg = np.zeros((TEXT_YSIZE, XSIZE, 3), np.uint8)
    for y, line in enumerate(text):
        cv2.putText(
            msg,
            line.strip(),
            (50, y * line_spacing + line_spacing),
            cv2.FONT_HERSHEY_TRIPLEX,
            fontScale=1.5,
            color=color,
            thickness=3
        )
    return msg


def prepare_index_arrays(c=300):
    """
    pre-calculate index arrays mapping each row
    from the text bitmap to the perspective.
    This greatly speeds up display.

    c : distance of the observer

    returns a dictionary of {y-position: numpy array}
    """
    indices = {}
    xx = np.arange(0, XSIZE)
    for yy in range(1, YSIZE):
        y = 1 - yy / YSIZE
        y0 = int(-y * c / (y - 1))

        x = xx - XSIZE // 2
        x0 = (x - (x * y) / (y - 1)).astype(int)
        x0 += 600

        idx = np.where((x0 >= 0) * (x0 < XSIZE))
        src = x0[idx]
        dest = xx[idx]
        indices[yy] = (y0, src, dest)
    return indices


msg = create_text_bitmap('message.txt', YELLOW)
indices = prepare_index_arrays()

ofs = 0  # vertical shift of the text
background = np.zeros((YSIZE, XSIZE, 3), np.uint8)
frames = []

while True:
    # display frames
    frame = background.copy()
    for yy in range(1, YSIZE):
        y0, src, dest = indices[yy]
        if 0 <= y0 < YSIZE:
            frame[yy][dest] = msg[-y0 + ofs][src]
    
    cv2.imshow('frame', frame)
    ofs += 1

    key = chr(cv2.waitKey(1) & 0xFF)
    if key == 'q':
        break

    # shrink frame for using it on the web
    #rgb = cv2.cvtColor(frame[200::2, 100:-100:2], cv2.COLOR_BGR2RGB)
    #frames.append(rgb)

    #time.sleep(0.03)

cv2.destroyAllWindows()
# imageio.mimsave('sw_animation.gif', frames, fps=20)