machine_stars.py

If you read our guide on standard I/O, you might remember this little metaphor for a filter program like stars.py that takes standard input and produces transformed output:

I just wrote a program called machine_stars.py that uses Pygame to animate this idea like so:

Clone the Adafruit-RasPipe Repo with Git

In order to experiment with this program and the other code covered in this guide, you should get a copy on your Raspberry Pi. Open up a terminal and enter the following:

Download: file
git clone https://github.com/adafruit/Adafruit-RasPipe.git

What just happened? Well, Git is a version control system, a tool used to record the changes made to a repository of files over time. GitHub is a popular hosting service for Git repositories. (In addition to cloning a local copy onto your own computer, you can browse all of the code used in this guide on GitHub.)

You should now have a directory with some Python files and other miscellaneous things in it. Have a look around:

Download: file
cd Adafruit-RasPipe
ls

In order to test machine_stars.py, you can do something like the following:

Download: file
shuf /usr/share/dict/words | head -50 | ./machine_stars.py

This will shuffle a long list of dictionary words, chop off the first 50, and pass them to the input of machine_stars.py, which should animate them falling into a little machine and being transformed into stars.

If you're using an Adafruit PiTFT display, do the following instead:

Download: file
export SDL_FBDEV=/dev/fb1
shuf /usr/share/dict/words | head -50 | ./machine_stars.py

Line by Line

Open up machine_stars.py in Nano and have a look:

Download: file
nano machine_stars.py

Let's step through this program a chunk at a time.

Download: file
#!/usr/bin/env python

import sys
import random
  
import pygame

The first line is a shebang. It tells the Linux kernel to run the rest of the program using /usr/bin/python.

The next three are imports, telling Python that we'll be using modules or libraries which contain code already written for us.

  • sys contains various ways to interact with the larger system - in this case stdin and stdout.
  • random lets us grab pseudo-random numbers so we can vary certain parts of our program.
  • pygame lets us do lots of things with graphics and sound.
Download: file
text_color = pygame.Color(0, 0, 0)
bg_color = pygame.Color(255, 255, 255)

pygame.init()
screen = pygame.display.set_mode([320, 240])
screen.fill(bg_color)

These lines build the initial state of our program, starting the Pygame engine with pygame.init(), and creating a 320x240 pixel screen, then painting it white.

pygame.Color(255, 255, 255) returns a Color object with its red, green, and blue values set to the maximum of 255. Each of these can be changed to anything between 0 and 255.

pygame.display.set_mode() actually returns a Surface object. You don't need to worry about that very much now, but later you'll want to remember that in Pygame, Surfaces are how you work with images in general.

Download: file
# Set up the picture of our little machine:
machine_img = pygame.image.load('machine.png').convert_alpha()
machine_img = pygame.transform.smoothscale(machine_img, (100, 112))
machine_rect = machine_img.get_rect()
machine_rect.left = 10
machine_rect.top = 120
screen.blit(machine_img, machine_rect)

# Set up the picture of a star:
orig_star_img = pygame.image.load('star.png').convert_alpha()

Next we load some image files. These are Surfaces too!

With pygame.image.load('machine.png'), we get a Surface for the file machine.png. The .convert_alpha() method returns a version of this image converted to display faster on our screen, complete with transparency - it's not strictly necessary, but it might make things run smoother.

If you have a Surface, you can get an object that represents the rectangle it occupies with get_rect(). In turn, you can position the Surface by changing values like machine_rect.left. Here, we set the left edge of our machine image at 10 pixels from the left of the screen and the top at 120 pixels from the top.

screen.blit(machine_img, machine_rect) draws the machine image on top of the screen Surface, at the coordinates provided by machine_rect. We only need to do this once, because everything else we draw will be on different parts of the screen.

We also get an image of a star. We'll display copies of this a bunch of times later on.

Download: file
# This will hold some input lines:
stars_length = 0
offset = 0

# Start building a list of things to display from stdin:
display_lines = [sys.stdin.readline()]

stars_length will be the number of stars to display - more about that in a while.

offset will be used later to offset the falling words from the top of the screen.

display_lines = [sys.stdin.readline()] creates a list with one element: The first line in our standard input. sys.stdin.readline() gets the stdin one line at a time.

Download: file
while len(display_lines) > 0:

Next, we start a loop. While the length of the display_lines list is greater than zero, everything in the following indented section will be executed repeatedly.

Each trip through this loop will serve two purposes:

  1. Get the next line of standard input, if there is one.
  2. Perform one frame of animation.
Download: file
    # Get the next available line from stdin:
    line = sys.stdin.readline()

    if line:
        display_lines.insert(0, line)

First, we get the next line of stdin.

Next, we check the truth value of line. The sys.stdin.readline() call will only return an empty string if there's no more input, and Python considers empty strings false, so we put the value of line at the front of the display_lines list only if it contains new input.

Download: file
    # If there're more than 6 lines to display, or we're not getting
    # any more input, pop the last line off the list and turn it into
    # a number of stars to show:
    if (len(display_lines) > 6) or (not line):
        stars_length = len(display_lines.pop())

    # If there's no more input, start offsetting display from the top
    # of the screen so it seems to fall downwards:
    if not line:
        offset = offset + 20

Next, we're going to decide how many stars to display. If we have more than six lines to display, or we're not going to get any more lines, we'll pop() the last line off of display_lines, then set stars_length to the number of characters it contains.

(The different ways we use a len() call here may be a bit confusing: In Python, it's a built-in function used to get the length of many different types of things, including both lists like display_lines and strings like the individual elements contained in display_lines.)

If we got an empty string for line, we'll also increment a variable called offset, which we'll use to push the lines of text further down the screen once no more are coming in from stdin. This makes it look like they're falling towards the machine.

Download: file
    # Blank the areas above and right of the machine image:
    screen.fill(bg_color, [0, 0, 320, 120])
    screen.fill(bg_color, [machine_rect.right, machine_rect.top, 320, 240])

    # Display the most recent lines of stdin falling into the machine,
    # in a font that gets smaller as it falls:
    font_size = 22
    y = 0 + offset
    for render_me in display_lines:
        font_size = font_size - 2
        font = pygame.font.Font(None, font_size) 
        input_text_surface = font.render(render_me.rstrip(), True, text_color)
        input_text_rect = input_text_surface.get_rect(center=(64, y))
        screen.blit(input_text_surface, input_text_rect)
        y += 20

    pygame.display.update()

Now we render the contents of display_lines falling towards the machine. The heart of this code is a for loop. render_me is set equal to each element of display_loop in turn, and the indented code is executed with that value.

Inside the loop, we use pygame.font.Font() to get a Font object which can render a Surface containing text, and then (just like with the machine image above), draw this on top of the screen, centered at 64 pixels from the left and y pixels from the top of the screen.

We call render_me.rstrip() to get a version of the string with any whitespace chopped off (including newlines, which will usually be present).

Each time through the for loop, we increase y by 20.

After all of display_lines has been rendered, we update the display with this frame of the animation.

Download: file
    # Display stars leaving machine's output.  Stars are scaled to a random
    # height & width between 8 and 30 pixels, then displayed at a random
    # vertical location on the screen +/- 8 pixels from 185:
    if stars_length > 0:
        star_x = machine_rect.right
        for i in range(0, stars_length):
            star_w = random.randrange(8, 30)
            star_h = random.randrange(8, 30)
            star_img = pygame.transform.smoothscale(orig_star_img, (star_w, star_h))

            star_rect = star_img.get_rect()
            star_rect.left = star_x
            star_rect.top = 185 + random.randrange(-8, 8)
            screen.blit(star_img, star_rect)

            pygame.display.update()

            # Chill out for 15 milliseconds:
            pygame.time.wait(15)

            # Move start of next star to end of the current one, and quit
            # drawing stars if we've run off the edge of the screen:
            star_x += star_w
            if star_x > 320:
                break

    pygame.time.wait(100)

Finally, we'll show stars_length stars emerging from the side of the machine.

star_x = machine_rect.right sets the first star's horizontal position to the righthand side of the machine.

for i in range(0, stars_length) starts a loop that will run stars_length times.

Inside the loop, we use random.randrange() to get stars of different widths and heights each time through, where pygame.transform.smoothscale(orig_star_img, (star_w, star_h)) makes a smoothly scaled copy of the original star image. We also randomize each star's vertical placement a bit.

pygame.time.wait(15) pauses for 15ms between each star, and pygame.time.wait(100) waits for 100ms before the main while loop starts over.

You can play with all of these timing and randomization values to get different effects. You should also be able to pipe just about any text in. Try, for example:

Download: file
dmesg | tail -10 | ./machine_stars.py
This guide was first published on Mar 20, 2015. It was last updated on Mar 20, 2015. This page (machine_stars.py) was last updated on Apr 12, 2019.