Tutorial 9: Simple Animation

Continuous Drawing

In the Drawing with Functions tutorial we saw how it is possible to create our own functions that we can call. In that tutorial we looked at one of the special functions that Processing defines, called setup() that is automatically called when a Processing sketch starts. Processing defines other functions that also get called automatically. One of the most important is called draw() and, if defined, it is called every time the display should be updated. By including this function in our sketches we can quickly write sketches that produce animation through continuous redrawing of the display. The following sketch shows a very simple example where an ellipse is redrawn each frame at a new position, based on the value of the variable y.

  • Show Sketch
/** @peep sketchcode */
float y = 0;
 
void setup() {
  size(200, 200);
  background(204);
}
 
void draw() {
  ellipse(width/2, y, 40, 40);
  y += 0.5;
}

Notice that for the value of y to be retained between calls to draw() we need to declare it outside of setup() and draw(). You might also notice that the end result is the same as if we had drawn the ellipse using a for loop:

  • Show Sketch
/** @peep sketchcode */
 
size(200, 200);
background(204);
 
float y = 0;
for (int i = 0; i < 4000; i++) {
  ellipse(width/2, y, 40, 40);
  y += 0.5;
}

This can be a useful way to think about the draw() function and how we can animate with it, i.e., using the draw() function to update a variable and then draw with it, is like unrolling a for loop over time.

Counting the frames

Processing provides a variable called frameCount that holds the number of frames that have been drawn so far, i.e., the total number of times that draw() has been called. We can use the frameCount variable in different ways, for example we can calculate the value of y within the draw() function using frameCount rather than keeping track of it with a "global" variable, like this:

  • Show Sketch
/** @peep sketchcode */
void setup() {
  size(200, 200);
  background(204);
}
 
void draw() {
  float y = frameCount * 0.5;
  ellipse(width/2, y, 40, 40);
}

There's no "right" or "wrong" way to calculate the value of y, both ways that we've looked at so far are good. Sometimes we might use a global variable and other times it might be easier to calculate the value based on frameCount.

Setting the animation speed

Processing also provides a function called frameRate(), which allows the maximum number of frames per second to be specified. So that calling frameRate(5) will limit Processing to drawing a maximum of 5 frames per second. This can be very useful if we want to slow down an animation so that we can get a better look at it. For example, we might increase the size of the step that y increases by each frame to 3.0, but reduce the number of frames per second to just 5 to see how the animation looks while maintaining the same rate of speed.

  • Show Sketch
/** @peep sketchcode */
void setup() {
  size(200, 200);
  background(204);
  frameRate(5);
}
 
void draw() {
  float y = frameCount * 3.0;
  ellipse(width/2, y, 40, 40);
}

Try setting the speed of the animation to different values by changing the value passed to frameRate().

Notice that frameRate() only needs to be called once to set the speed of the animation. It is also important to understand that calling frameRate() cannot force an animation to run more quickly, it can only limit the maximum speed.

Note: in older versions of Processing the function was called framerate() but the name was changed to match the naming convention of other functions in Processing. You may find older code still uses this spelling and you will have to change the spelling of the function call before the code will run.

Clearing the display

Notice that the ellipse in the sketch above leaves a trail on the display. This is because Processing doesn't clear the display window unless we tell it to. If we want the window to be cleared we can call the background() function each time the draw() is called before we draw the new drawing. To change the above sketch all we have to do is move the call to background() from setup() to draw():

  • Show Sketch
/** @peep sketchcode */
float y = 0;
 
void setup() {
  size(200, 200);
}
 
void draw() {
  background(204);
  ellipse(width/2, y, 40, 40);
  y += 0.5;
}

Now the ellipse looks like it's animating on the default grey background.

Fading the background

A useful trick, for debugging or as a visual effect, is to draw a rectangle over the whole display with a low opacity value. This takes a little more code, because we have to set the fill and stroke values for the rectangle and then set them back to draw the ellipse, but it's still simple to understand:

  • Show Sketch
/** @peep sketchcode */
float y = 0;
 
void setup() {
  size(200, 200);
  background(0);
  noStroke();
}
 
void draw() {
  fill(0, 8);
  rect(0, 0, width, height);
  fill(255);
  ellipse(width/2, y, 40, 40);
  y += 0.5;
}

We're not limited to fading the background to black, in theory, we can fade to any colour we like using this technique. For example, we could fade the background to blue:

  • Show Sketch
/** @peep sketchcode */
float y = 0;
 
void setup() {
  size(200, 200);
  background(0, 102, 153);
  noStroke();
}
 
void draw() {
  fill(0, 102, 153, 8);
  rect(0, 0, width, height);
  fill(255);
  ellipse(width/2, y, 40, 40);
  y += 0.5;
}

Notice that the RGB values given to background() match those used in draw() but with addition of a small alpha value. If you set the initial background to a different colour the display window will fade over time to the RGB values provided in draw().

Experiment with different background colours. You may find that the results that you get for different colours and/or the same colour with different alpha values aren't always the same.

The above method doesn't provide much of a trail of the recent drawing, and (sadly) if we try to reduce the alpha value with which we draw the rectangle any further it will not work very well (feel free to try!).

One solution is to use the frameCount variable provided by Processing. If we check this value using the modulo (%) operator we can use the test to draw only every few frames in the following way:

  • Show Sketch
/** @peep sketchcode */
float y = 0;
 
int framesBetweenFades = 10;
 
void setup() {
  size(200, 200);
  background(0, 102, 153);
  noStroke();
}
 
void draw() {
  if (frameCount % framesBetweenFades == 0) {
    fill(0, 102, 153, 8);
    rect(0, 0, width, height);
  }
  fill(255);
  ellipse(width/2, y, 40, 40);
  y += 0.5;
}

Experiment with the value for framesBetweenFades. You should notice that as the delay between fades gets longer it becomes more obvious when a fade is being applied.

Motion Through Transformation

The transformation functions can also create motion by changing the parameters to translate(), rotate(), and scale().

Transformations reset at the beginning of each draw(). Calling translate(5, 0) will always move the coordinate system 5 pixels to the right in each frame. It will not move the system 5 right on the first frame, 10 on the next, 15 on the next etc. Consequently, to animate using transformations we need to update the amount of translation, rotation and scaling as variables that persist between calls to draw(). The following examples illustrate this for simple cases of translation and rotation.

  • Show Sketch
/** @peep sketchcode */
float y = 50.0;
float speed = 1.0;
float radius = 15.0;
 
void setup() {
  size(200, 200);
  background(0);
  noStroke();
  ellipseMode(RADIUS);
}
 
void draw() {
  fill(0, 12);
  rect(0, 0, width, height);
  fill(255);
  translate(0, y); // Set the y-coordinate of the circle
  ellipse(33, 0, radius, radius);
  y += speed;
  if (y > height + radius) {
    y = -radius;
  }
}
  • Show Sketch
/** @peep sketchcode */
float angle = 0.0;
 
void setup() {
  size(200, 200);
  background(0);
  noStroke();
}
void draw() {
  fill(0, 12);
  rect(0, 0, width, height);
  fill(255);
  translate(130, 120);
  rotate(angle);
  rect(-60, -60, 120, 120);
  angle = angle + 0.02;
}

Combine the techniques demonstrated in the above sketches to produce a looping animation of a rotating square falling down the screen.

Falling Rotating Square

Using pushMatrix() and popMatrix() we can isolate transformations relative to the current overall transformation, making it realtively simple to animate complex structures. For example, here's a sketch that draws two circles spiralling around each other:

  • Show Sketch
/** @peep sketchcode */
float y = -40.0;
float speed = 1.0;
float angle = 0.0;
float spin = 0.1;
 
void setup() {
  size(200, 200);
  background(0);
  noStroke();
}
 
void draw() {
  fill(0, 12);
  rect(0, 0, width, height);
 
  fill(255);
  translate(width/2, y);
  rotate(angle);
  pushMatrix();
  translate(30, 0);
  ellipse(0, 0, 20, 20);
  popMatrix();
  pushMatrix();
  translate(-30, 0);
  ellipse(0, 0, 20, 20);
  popMatrix();
 
  y += speed;
  if (y > height + 40) {
    y = -120;
  }
  angle = angle + spin;
}

Adapt and extend the sketch you wrote above to draw 4 smaller rectangles around the central rotating one. Use pushMatrix() and popMatrix() and position each smaller box with a translation, so that each box is drawn relative to it's own local coordinate system, i.e., around (0, 0). You should be able to create an animation that looks something like this:

Spinning and Falling Boxes

Note: In this image the size of the large rectangle has been reduced.

Try giving each box a unique colour:

Spinning and Falling Coloured Boxes

Because each smaller squares are drawn relative to their own (isolated) coordinate system, they can be individually transformed. Try to add a rotate() statement just before the smaller squares are drawn to make them spin as they turn around the big square:

Spinning and Falling Coloured Boxes

Remember to upload some of the sketches you create in the tutorials to a portfolio post. Especially if you create significant variations on the sketches provided

Periodic Motion

The sin() function is often used to produce elegant motion. It can generate an accelerating and decelerating speed as a shape moves from one frame to another.

  • Show Sketch
/** @peep sketchcode */
float angle = 0.0; // Current angle
float speed = 0.05; // Speed of motion
float radius = 80.0; // Range of motion
 
void setup() {
  size(200, 200);
  background(0);
  noStroke();
}
 
void draw() {
  fill(0, 12);
  rect(0, 0, width, height);
  fill(255);
  float yoffset = sin(angle) * radius;
  ellipse(width/2, height/2 + yoffset, 20, 20);
  angle += speed;
}

Adding values from sin() and cos() can produce more complex movement that remains periodic. In the following example, a small dot moves in a circular pattern using values from sin() and cos(). A larger dot uses the same values for its base position but adds additional sin() and cos() calculations to produce an offset.

  • Show Code
/** @peep sketch */
float angle = 0.0; // Current angle
float speed = 0.01; // Speed of motion
float radius = 60.0; // Range of motion
float sx = 2.0;
float sy = 2.0;
 
void setup() {
  size(200, 200);
  background(0);
  noStroke();
}
 
void draw() {
  fill(0, 2);
  rect(0, 0, width, height);
  angle += speed; // Update the angle
  fill(255);
  // Set the position of the small circle based on new
  // values from sine and cosine
  float x = width/2 + (cos(angle) * radius);
  float y = height/2 + (sin(angle) * radius);
  ellipse(x, y, 4, 4); // Draw smaller circle
  // Set the position of the large circles based on the
  // new position of the small circle
  float x2 = x + cos(angle * sx) * radius / 2;
  float y2 = y + sin(angle * sy) * radius / 2;
  ellipse(x2, y2, 12, 12); // Draw larger circle
}

Experiment with the values for sx and sy to explore different motion paths. Check the slides from the lecture on animation and motion to see examples of paths with associated values for sx and sy. Particularly interesting effects can be achieved if the values for sx and sy are prime, e.g., 5 and 7. (As you increase the values of sx and sy you may want to reduce the value for speed to maintain a continuous trace on the screen.)

Periodic Motion using Transformations

A simpler implementation of a similar effect to the above can be achieved using transformations.

  • Show Sketch
/** @peep sketchcode */
float angle1 = 0.0; // Current angle for inner wheel
float speed1 = 0.025; // Speed of motion for inner wheel
float radius1 = 60.0; // Radius of motion for inner wheel
float angle2 = 0.0; // Current angle for outer wheel
float speed2 = 0.075; // Speed of motion for outer wheel
float radius2 = 30.0; // Radius of motion for outer wheel
 
void setup() {
  size(200, 200);
  background(0);
  noStroke();
}
 
void draw() {
  fill(0, 2);
  rect(0, 0, width, height);
  fill(255);
  angle1 += speed1; // Update the angle of inner wheel
  angle2 += speed2; // Update the angle of outer wheel
  translate(width/2, height/2);
  rotate(angle1);
  translate(radius1, 0);
  ellipse(0, 0, 4, 4); // Draw smaller circle
  rotate(angle2);
  translate(radius2, 0);
  ellipse(0, 0, 12, 12); // Draw larger circle
}

Notice that this isn't the same because we can't vary the scaling values for x and y independently, however, if you are familiar with the children's toy Spirograph then the patterns that this sketch creates will look similar. As with the Spirograph, the pattern generated is a consequence of the ratio between the speed of motion of the inner and outer circles.

Experiment with the values for speed1 and speed2 to see what sorts of patterns you can produce.

In the Spirograph the speed of motion is controlled by the number of teeth on each wheel but it also controls the the radius of each wheel. In this virtual system the two can be easily decoupled.

We can extend this sketch to do other things that would be difficult with a physical system, for example, we can easily add a third "wheel" that turns around the second to produce more complex animated paths. In the version below the drawing of the inner circles has been disabled to focus on the path drawn by the outer-most circle:

  • Show Code
/** @peep sketch */
float angle1 = 0.0; // Current angle for inner wheel
float speed1 = 0.01; // Speed of motion for inner wheel
float radius1 = 50.0; // Radius of motion for inner wheel
float angle2 = 0.0; // Current angle for middle wheel
float speed2 = 0.03; // Speed of motion for middle wheel
float radius2 = 20.0; // Radius of motion for middle wheel
float angle3 = 0.0; // Current angle for outer wheel
float speed3 = 0.07; // Speed of motion for outer wheel
float radius3 = 20.0; // Radius of motion for outer wheel
 
void setup() {
  size(200, 200);
  background(0);
  noStroke();
}
 
void draw() {
  fill(0, 2);
  rect(0, 0, width, height);
  fill(255);
  angle1 += speed1; // Update the angle of inner wheel
  angle2 += speed2; // Update the angle of middle wheel
  angle3 += speed3; // Update the angle of outer wheel
  translate(width/2, height/2);
  rotate(angle1);
  translate(radius1, 0);
  // ellipse(0, 0, 4, 4); // Draw smaller circle
  rotate(angle2);
  translate(radius2, 0);
  // ellipse(0, 0, 8, 8); // Draw medium circle
  rotate(angle3);
  translate(radius3, 0);
  ellipse(0, 0, 12, 12); // Draw larger circle
}

Experiment with changing the relative speeds and radii of the circles to explore the different paths that can be traced out using this approach.

Notice that in the above code we're repeating the update and transformation code three times, once for each "wheel". We can refactor the code to make use of arrays very easily:

  • Show Sketch
/** @peep sketchcode */
float[] angles = {0.0, 0.0, 0.0};
float[] speeds = {0.01, 0.03, 0.07};
float[] radii = {50.0, 20.0, 20.0};
 
void setup() {
  size(200, 200);
  background(0);
  noStroke();
}
 
void draw() {
  fill(0, 2);
  rect(0, 0, width, height);
  fill(255);
  // Update the wheel angles
  for (int i = 0; i < angles.length; i++) {
    angles[i] += speeds[i];
  }
  // Transform to the outer wheel position
  translate(width/2, height/2);
  for (int i = 0; i < angles.length; i++) {
    rotate(angles[i]);
    translate(radii[i], 0);
  }
  // Draw larger circle
  ellipse(0, 0, 12, 12);
}

Experiment with this code and see if you can add another wheel to the Spirograph.

Can you extend the sketch to draw the different wheels in different colours? Try to create a sketch that can draw complex animated patterns using this technique:

Coloured Spirograph

Phase Shifting

The phase of a function is one iteration through its possible values, for example, a single rise and fall sequence of a sine curve. Phase shifting occurs when a function is offset to start at a different point within the phase, e.g., starting at an angle other than zero for sine.

  • Show Sketch
/** @peep sketchcode */
float angle = 0.0; // The current angle passed to sin() to calculate the x
float speed = 4; // The angular speed (in degrees)
float shift = 60; // The shift between the phases of the circles (in degrees)
float radius = 30; // The radius of the circles to draw
 
void setup() {
  size(200, 200);
  background(0);
  noStroke();
}
 
void draw() {
  background(0);
  angle = angle + speed;
  translate(width/2, height - radius);
  float phase = 0;
  for (int i = 0; i < 3; i++) {
    float x = 20 * sin(radians(angle + phase));
    ellipse(x, 0, 2*radius, 2*radius);
    translate(0, -2*radius);
    phase += shift;
  }
}

The code for this sketch is similar to the code presented in the lecture but it makes use a local variable to the draw() function called phase that accumulates the phase shifts as the for loop is executed. It also introduces the radius variable to make it easy to change the sketch.

Experiment with the shift variable to see what the effect of using different phases is on the animation.

Reduce the size of the circles and increase the number of circles being drawn to see how the animation looks.

Experiment with the fading of the background and the drawing of the circles with opacity to try to reproduce an animation that looks like this:

Faded Phase Shift

Using Arrays to Create Movement

Storing the coordinates of many elements is another way to use arrays to make a program easier to read and manage. In the following example, the x[] array stores the x-coordinate for each line, and the speed[] array stores the amount to add to x at each frame. Using arrays allows the code to be shorter and more flexible; changing the value assigned to numLines sets the number of elements drawn to the screen.

  • Show Sketch
/** @peep sketchcode */
int numLines = 40;
float[] x = new float[numLines];
float[] speed = new float[numLines];
float offset = 5; // Set space between lines
 
void setup() {
  size(200, 200);
  strokeWeight(10);
  for (int i = 0; i < numLines; i++) {
    x[i] = i; // Set initial position
    speed[i] = 0.1 + (i/offset); // Set initial speed
  }
}
 
void draw() {
  background(204);
  for (int i = 0; i < x.length; i++) {
    x[i] += speed[i]; // Update line position
    float y = i * offset; // Set y-coordinate
    line(x[i]%width, y, x[i]%width + offset, y + offset);
    line(x[i]%width - width, y, x[i]%width - width + offset, y + offset);
    line(x[i]%width + width, y, x[i]%width + width + offset, y + offset);
  }
}

The code that draws the lines in this animation is important to understand. The rest of this tutorial is based on a forum post that takes this code apart. So let's look at the first of the lines where we do the actual drawing:

    line(x[i]%width, y, x[i]%width + offset, y + offset);

    the expression x[i]%width takes the current value of x[i], which may be much larger than the width of the window after the sketch has been running for a little while and calculates a position on screen using the modulo (%) operator. We use this twice, to calculate the start and end position of the line, adding offset to calculate the end position. The vertical positioning is very much the same.

    The following two lines are very similar, the only difference is that they calculate positions that are exactly the width of the display window ahead and behind of the current position, i.e., plus and minus width.

      line(x[i]%width - width, y, x[i]%width - width + offset, y + offset);
      line(x[i]%width + width, y, x[i]%width + width + offset, y + offset);

      So the question must be "Why bother?", surely these lines must be off the screen and therefore invisible!

      To understand why this code is necessary let's see how the sketch looks without these additional lines that I've slowed this one down so that we can see more clearly what's happening:

      • Show Code
      /** @peep sketch */
      int numLines = 20;
      float[] x = new float[numLines];
      float[] speed = new float[numLines];
      float offset = 5; // Set space between lines
      void setup() {
        size(100, 100);
        frameRate(10);
        strokeWeight(10);
        for (int i = 0; i < numLines; i++) {
          x[i] = i; // Set initial position
          speed[i] = 0.1 + (i/offset); // Set initial speed
        }
      }
       
      void draw() {
        background(204);
        for (int i = 0; i < x.length; i++) {
          x[i] += speed[i]; // Update line position
          float y = i * offset; // Set y-coordinate
          line(x[i]%width, y, x[i]%width + offset, y + offset);
        }
      }

      It looks almost exactly the same! But notice that the lines "pop" at the edges of the screen, i.e., suddenly appearing at the left hand side when they disappear at the right hand side, as the modulo operator is applied. This is why we need to draw the additional two lines! Compare it with the original version, similarly slowed down:

      • Show Code
      /** @peep sketch */
      int numLines = 20;
      float[] x = new float[numLines];
      float[] speed = new float[numLines];
      float offset = 5; // Set space between lines
      void setup() {
        size(100, 100);
        frameRate(10);
        strokeWeight(10);
        for (int i = 0; i < numLines; i++) {
          x[i] = i; // Set initial position
          speed[i] = 0.1 + (i/offset); // Set initial speed
        }
      }
       
      void draw() {
        background(204);
        for (int i = 0; i < x.length; i++) {
          x[i] += speed[i]; // Update line position
          float y = i * offset; // Set y-coordinate
          line(x[i]%width, y, x[i]%width + offset, y + offset);
          line(x[i]%width - width, y, x[i]%width - width + offset, y + offset);
          line(x[i]%width + width, y, x[i]%width + width + offset, y + offset);
        }
      }

      Go back to the original example with three lines being drawn and try to pause the sketch just as a line is reaching the right hand side of the screen, what you'll see is that there are two lines visible at the same time on a single line. These lines will be spaced exactly width apart.

      To make this clearer still, I've written a modified version of the above sketch to show what' happening outside the display window (don't worry about trying to understand the code in this sketch, as it's been changed significantly to create the additional space outside the display window). I've slowed this one down as well so that you can see how it works, notice that the lines still pop at the left and right edges of this sketch, but where the display window would normally start it's all smooth...

      • Show Code
      /** @peep sketch */
      int numLines = 20;
      float[] x = new float[numLines];
      float[] speed = new float[numLines];
      float offset = 5; // Set space between lines
      int w;
      void setup() {
        size(300, 100);
        frameRate(10);
        w = 100;
        smooth();
        strokeWeight(10);
        for (int i = 0; i < numLines; i++) {
          x[i] = i; // Set initial position
          speed[i] = 0.1 + (i/offset); // Set initial speed
        }
      }
       
      void draw() {
        background(204);
        stroke(0);
        for (int i = 0; i < x.length; i++) {
          x[i] += speed[i]; // Update line position
          float y = i * offset; // Set y-coordinate
          line(w + x[i]%w, y, w + x[i]%w + offset, y + offset);
          line(w + x[i]%w - w, y, w + x[i]%w - w + offset, y + offset);
          line(w + x[i]%w + w, y, w + x[i]%w + w + offset, y + offset);
        }
        noStroke();
        fill(204, 192);
        rect(0, 0, w, height);
        rect(w+w, 0, w, height);
      }

      Using these techniques it is possible to create animations that smoothly wrap around the display window.

      Experiment with the original sketch for this chapter and substitute different values to change the movement and/or change the drawing.

      Comments

      Nobody has said anything yet.