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.
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()
andpopMatrix()
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:
Note: In this image the size of the large rectangle has been reduced.
Try giving each box a unique colour:
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:
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
andsy
to explore different motion paths. Check the slides from the lecture on animation and motion to see examples of paths with associated values forsx
andsy
. Particularly interesting effects can be achieved if the values forsx
andsy
are prime, e.g.,5
and7
. (As you increase the values ofsx
andsy
you may want to reduce the value forspeed
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
andspeed2
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:
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:
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:
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.