✿ code toolkit ✿ python ✿

spring 2022



Week 6 Notes



Timing and st✿te

We've already seen how to draw shapes and make them move on their own. This motion would typically start when the program started running and continue forever. Or maybe it would be triggered or changed by user interaction in some way. Today we are going to talk about timing: how to create motion that is in some way scheduled, chorreographed, triggered with some delay, or that repeats at some interval.

Time-based media are most typically thought of as sound and the moving image, although digital media like video games and motion graphics are also grouped in. Today we will see explicitly how software operates in a time-based manner, and think about how to work with the domain of time, as we have thus far mainly been working within the domain of space.

History and inspiration

Oskar Fischinger

One incredible example of early time-based media is the work of Oskar Fischinger. Fischinger was an artist in the first part of the 20th century and a pioneer in animation and motion graphics. A classic example of his work is the animated film Studie nr 8, 1931.

You might be inclined to think of the history of animation as preceding that of cinema, but in fact there is a historical case to be made for understanding these in the opposite order. Several modern artists and illustrators in the early 20th century were inspired by cinema to create animation, and used the principle of cinema — a continuous strip of film, divided into cells of static imagery that flicker by the viewer's eyes — to create early animation and motion graphics. In fact, many of these innovators actually worked on film strips, drawing and etching into the cells by hand.

Fischinger actually contributed to Disney's Fantasia but quit uncredited because of creative differences. In 2012, the Whitney museum showed an exhibition of his artwork called "Space Light Art". There is great documentation for this show online and I highly recommend taking a look!

A numeric timeline with millis()

In week 4 ("Making Things Move") we saw how you could use variables and some basic arithmetic to make things move "on their own".

We looked at this pattern:

circleX = 300
def setup():
 size(600,600)
 stroke(50,50,150)
 fill(200,200,255)

def draw():
 global circleX
 background(255)
 ellipse( circleX,300, 50,50)
 circleX = circleX + 1

That draws a single circle, and to make the circle move horizontally, it create a variable which gets used to specify the x position of the circle. The value of this variable is then changed a little every frame, by incrementing or decrementing the variable. We expanded on this example using conditionals to ask questions about the position of the circle, and changed its movement based on that. For example:

circleX = 300
def setup():
 size(600,600)
 stroke(50,50,150)
 fill(200,200,255)

def draw():
 global circleX
 background(255)
 ellipse( circleX,300, 50,50)
 circleX = circleX + 1
 if circleX > width:
     circleX = 0

Today we are going to build on this, but instead of using conditionals to ask questions about movement in space, we're going to use them to ask questions about time.

Unlike other time-based digital tools — like Adobe Premiere, After Effects, Audacity, or others — Processing does not give you an explicit, visual timeline. If you want to create timed events, you have to think in terms of numbers.

Processing gives us a command just for this purpose: millis() (check the reference)

This command returns a number that corresponds to the number of milliseconds since the program started running. A millisecond is one thousandth of a second, so 1000 milliseconds = 1 second. millis() is like a stopwatch that starts when your program starts, and keeps running as long as the program is running.

Even though this isn't a visual timeline, we can imagine a visual timeline in our thinking. millis() returns a number that represents a playhead or a marker, moving along a timeline. Thus, we can use this like a variable to ask questions about the status of our sketch, similar to mouseX and mouseY. We can also use it to "save" or remember a position of this playhead on the timeline.

This one basic command is enough to implement many different types of timed events, from simple things up to more complicated ones.

For example, think of this pseudocode:

Do something for the first three seconds my sketch is running
We could implement that with:
 if millis() < 3000:
     # Do something
Similarly, think about this pseudocode:
Wait three seconds, then do something for two seconds
Which we could implement with this:
 if millis() > 3000 and millis() < 5000:
     # Do something

What should Do something be in this case? Well let's say we want to draw a shape and only have it move according to the above timing. We could combine movement and timing like this:

circleX = 300
def setup():
 size(600,600)
 stroke(50,50,150)
 fill(200,200,255)

def draw():
 global circleX
 background(255)
 ellipse( circleX,300, 50,50)
 if millis() < 3000:
   circleX = circleX + 1

 if circleX > width:
   circleX = 0

Notice how now, the circle's position is being updated just like before, but only when that conditional about millis() is True. In other words, only during the interval described by the Boolean expression millis() < 3000 We can get a bit more complicated by introducing variables to use for saving time values. For example:

circleX = 300
startTime = 3000
def setup():
 size(600,600)
 stroke(50,50,150)
 fill(200,200,255)

def draw():
 global circleX
 background(255)
 ellipse( circleX,300, 50,50)
 if millis() > startTime and millis() < startTime + 2000:
   circleX = circleX + 1

 if circleX > width:
   circleX = 0

When would this move the circle?

This doesn't seem that interesting since I'm setting the variable startTime to a hard-coded value. But because I am using a varibale as a placeholder for that value, that means that the value could change.

Building on this, we can combine it with event handling to let the user trigger the timing:

circleX = 300
startTime = 3000
def setup():
 size(600,600)
 stroke(50,50,150)
 fill(200,200,255)

def draw():
 global circleX
 background(255)
 ellipse( circleX,300, 50,50)
 if millis() > startTime and millis() < startTime + 2000:
   circleX = circleX + 1

 if circleX > width:
   circleX = 0

 print(millis(), startTime + 2000)

def keyPressed():
 global startTime
 startTime = millis()

Now when would this move the circle? Think about when the startTime variable gets set. Whenever any key is pressed, startTime gets set to the current value of millis() — or in other words, it marks where the imaginary playhead is at when the user presses a key. Then, each time draw() runs, it checks if the current value of millis() is greater than startTime, and less than startTime plus 2000, or two seconds. So, in pseudocode we could say:

Every time the user presses any key,
 move a circle to the right for two seconds.

Notice that the circle still starts moving at 3 seconds and moves for 2 seconds. This is because of the initial value that we are using for startTime, still carried over from the previous code snippet. What could you set the initial value of startTime to so that it would not appear like this? Think about when that conditional is True and what value would make it not True.

The following three examples show how to do different cases. Hopefully you can use these code snippets as patterns for how to implement these situations. Use them, combine them with other things we've worked on, and build on them.

Wait a while, then do something forever

In this example we wait two seconds, and then start moving the circle and never stop

position = 0

def setup():
 size(600,600)
 smooth()
 stroke(50,50,150)
 fill(200,200,255)

def draw():
 global position
 background(255)

 ellipse(position,300,100,100)

 # wait two seconds, then move forever:
 if millis() > 2000:
     position = position + 1

Wait a while, then do something for a while, then stop

In this example we wait two seconds, then start moving the circle, move the square for one second, and then stop.

position = 0

def setup():
 size(600,600)
 smooth()
 stroke(50,50,150)
 fill(200,200,255)

def draw():
 global position
 background(255)

 ellipse(position,300,100,100)

 # only increase the position if millis() is greater than two seconds but
 # less than three seconds
 if millis() > 2000 and millis() < 3000:
     position = position + 1

Do something for a little while, then stop
Wait a little while and repeat the above

This example is similar to example 2 above, but it has a variable which gets added to the timing values. Then another if statement periodically resets that variable. This causes the whole timing process to reset, doing this forever.

position = 0

startTime = 0

def setup():
 size(600,600)
 smooth()
 stroke(50,50,150)
 fill(200,200,255)

def draw():
 global position, startTime
 background(255)

 ellipse(position,300,100,100)

 # move for a little while, then stop
 if millis() > startTime and millis() < startTime + 2000:
     position = position + 1

 # wait a little while, then reset the startTime variable,
 # so the above timing starts over:
 if millis() > startTime + 4000:
   startTime = millis()






State: a new way to use variables

So far we have been using variables either for numeric things (like shape sizes, positions, or colors) or for Boolean True/False values. All of these things are ways of keeping track of what the program is doing in a given moment.

Today we saw how we could use timing and millis() to affect how the program might change over time.

But what if we want the program to change over time in a way that is not explicitly tied to timing. For example, what if we wanted the user to be able to change what the program was doing? Or if we want the program to operate in different phases or modes, and move through those?

In computer science, the term for keeping track of the status of the program is state. We might ask, what state or phase is the program in? This could be used to implement the levels of a video game for example.

The way to do this is by using variables. This is not very different from anything that we have been doing so far. I just want to show a few examples that emphasize how you can use variables in some slightly different ways.

Let's implement a simple light switch. When the user presses the 'n' key (for "on") the light goes on, and when the user presses the 'f' key (for "off") the light goes off.

Let's start by simply drawing a window "with the light off" (black screen):

To implement the "on" key swe could use the Boolean keyPressed variable, like this:


def setup():
 size(800,800)

def draw():
 background(0)

 if keyPressed and key == 'n':
     background(255,255,200)

But that only works if the user is holding down the 'n' key. What if we want this to work not like a button that must be held down, and more like a switch that can be flipped?

Could we improve on things by using the def keyPressed() block? That would look like this: block,

def setup():
 size(800,800)

def draw():
 background(0)

def keyPressed():
 if key == 'n':
     background(255,255,200)

That doesn't really seem to help things. Now, whenever the user presses the 'n' key, the light flashes on for a split second. It still doesn't stay on.

We need a way to keep track of whether the light has been pressed or not. In other words, we need to keep track of the state of the light switch. We'll do this with a variable. Since this variable will only have two values (on, or off) we can use a Boolean variable for this purpose. Initially I will set the variable to False to signify that the light is off.

switchState = False
def setup():
 size(800,800)

def draw():
 if switchState:
     background(255,255,200)
 else:
     background(0)

Now, whenever the variable switchState is True, I will draw the light as on, and whenever the variable is False, I will draw the light off. But this is not doing anything yet. Why? Because I am not changing this variable anywhere! Let's try to change it.

When the user presses the 'n' key, let's make the variable True:

switchState = False
def setup():
 size(800,800)

def draw():
 if switchState:
     background(255,255,200)
 else:
     background(0)

def keyPressed():
 global switchState
 if key == 'n':
     switchState = True

OK! This is getting is somewhere. Now when the user presses the 'n' key, switchState is set to True, and so the light will be drawn on.

The last thing is to let the user turn it off. Have a look at this:

switchState = False
def setup():
 size(800,800)

def draw():
 if switchState:
     background(255,255,200)
 else:
     background(0)

def keyPressed():
 global switchState
 if key == 'n':
     switchState = True
 if key == 'f':
     switchState = False

Great! So now we have a variable (switchState) that keeps track of whether the user has pressed a certain key or not. In other words, it is keeping track of the state of the program. And we can use if statements to change the value of that variable.

Of course, we can create programs with even more states than just on / off. As a simple example, we could repeat the pattern from the above example to have several different on / off variables. Let's create a program that let's the user turn three shapes on or off:

rectOn = False
ellipseOn = False
triangleOn = False

def setup():
 size(800,800)
 rectMode(CENTER)

def draw():
 background(255)
 if rectOn:
     fill(255,155,155)
     rect(200,400,50,50)

 if ellipseOn:
     fill(155,255,155)
     ellipse(400,400,50,50)

 if triangleOn:
     fill(155,155,255)
     triangle(600,375, 625,425, 575,425)

def keyPressed():
 global rectOn, ellipseOn, triangleOn

 if key == 'q':
     rectOn = True
 if key == 'a':
     rectOn = False
     
 if key == 't':
     ellipseOn = True
 if key == 'g':
     ellipseOn = False
     
 if key == 'o':
     triangleOn = True
 if key == 'l':
     triangleOn = False

Notice that I've named my variables using the pattern triangleOn. This creates a nice way of reading your code so that you're looking at the if statements, you can read it like If the triangle is on. What if we didn't want to have different keys for on and off, but instead wanted the same key to turn each shape on and off? We call this a toggle. And it looks like this:

rectOn = False
ellipseOn = False
triangleOn = False

def setup():
 size(800,800)
 rectMode(CENTER)

def draw():
 background(255)
 if rectOn:
     fill(255,155,155)
     rect(200,400,50,50)

 if ellipseOn:
     fill(155,255,155)
     ellipse(400,400,50,50)

 if triangleOn:
     fill(155,155,255)
     triangle(600,375, 625,425, 575,425)

def keyPressed():
 global rectOn, ellipseOn, triangleOn

 if key == 'q':
     rectOn = not rectOn
     
 if key == 't':
     ellipseOn = not ellipseOn
     
 if key == 'o':
     triangleOn = not triangleOn

Now, in those if statements, I'm setting each variable to the opposite of whatever it currently is. This creates the toggle effect.

Lastly, we can keep track state that is more than on / off. Have a look at this code:

dayEveningNight = 1

def setup():
 size(800, 800)

def draw():
 if dayEveningNight == 1:
     background(155,155,255)
 elif dayEveningNight == 2:
     background(25,25,75)
 elif dayEveningNight == 3:
     background(0)

def keyPressed():
 global dayEveningNight

 if key == 'q':
     dayEveningNight = dayEveningNight + 1
     if dayEveningNight > 3:
         dayEveningNight = 1

Here, I am using a variable dayEveningNight that is holding the values 1, 2, or 3. I am using an if statement in the draw() block that checks what the value of this variable is and draws something accordingly. And then, in the keyPressed() block, I am incrementing that variable based on what the user has pressed. If the variable gets larger than 3, I am reseting back to its initial value of 1.

You can use these same principles to keep track of many kinds of state within your program. For example, if a user is entering a password, or the levels of a game.





in-class excercise

  1. Create a composition using any draw commands that you wish. Create one or two global variables and use them in some way in your composition: perhaps to control the color, size, or location of some things that you're drawing. Inside the draw() block, change the variable(s) (incrementing or decrementing) by a small amount so that the composition somehow changes "on its own".

    Now, add one or two if statements using millis() and put your variable increment or decrement inside the if statement(s) so that the sketch only changes based on some timing.

    Using two variables, adjust the timing so that some things change while other things pause, and then alternates so the moving things pause and the other things move?

    Experiment so that this is either (a) triggered by user input (say, pressing a key), or (b) repeats in time.
  2. You can continue with the previous example or start something new. If continuing, please Save As and rename indicating that you are working on part 2. Create a composition that keeps track of state in some way. Can you add three different raster images that the user cycles through by pressing a key? Can you combine several things that we've talked about so far so that by pressing a key, the user toggles the speed at which something is moving?
  3. ** before the end of class, drop your work in your google drive folder, name it homework_6_yourname