Processing and Animation

Consider this sketch. What does it do? Try to figure it out by reading, and by running it.

void setup() {
  size(400, 400);
}
 
void draw() {
  background(220);
  noStroke();
  fill(255, 0, 0);
  circle(100, 100, 50);
}

There are several possible answers, from less to more specific:

  1. It draws something.
  2. It draws a shape.
  3. It draws a circle.
  4. It draws red circle over a gray background.
  5. It draws red circle over an 86% (220 / 255) gray background.
  6. It repeatedly draws: an 86% gray background, and a red circle on top.

Take particular note of the difference between descriptions #5 and #6. This sketch draws the background and then the circle over and over again. It does this because the instructions to do this are inside of draw(), and Processing calls (runs) draw() over and over again.

Also note that at no time do we see just the background, without the circle. This is because the execution model for Processing looks like this:

image
image

Now consider this modification.

int x = 100;

void setup() {
  size(400, 400);
}

void draw() {
  background(220);
  noStroke();
  fill(255, 0, 0);
  circle(x, 100, 50);
}

This has exactly the same behavior. It has a different implementation. In the new sketch, the horizontal (x) position of the circle is the value of the x variable.

x is a variable that we defined. Because we define it at the top of the sketch, we can use it in setup() and draw().

There are a few variables that Processing itself defines. Consider this modification, that removes the definition of x, and changes the call to circle() to use mouseX instead of x. mouseX is one of the variables that Processing defines – this is why we can use it without defining it ourselves. You can read about what it does in the Processing reference, but you can probably also figure this out by interacting with the sketch. (Hint: move the mouse over the sketch window.)

void setup() {
  size(400, 400);
}

void draw() {
  background(220);
  noStroke();
  fill(mouseX, 0, 0);
  circle(mouseX, 100, 50);
}

One way to visualize what is happening is to draw a data flow diagram. The diagram on the left shows the sketch with the variable x. This variable gets a value when we assign to it: x = 10. The diagram on the right shows what happens when we use mouseX. Each time before draw() is called, it gets a new value by reading the position of the physical mouse (or trackpad) that is connected to the computer. (This is something Processing does, so it happens automatically so far as your sketch is concerned.) (This may remind you of reading the value of a potentiometer in an Arduino sketch.)

image
image

We could add more detail to this second diagram, to show all the values, and all the arguments to fill() and circle(). (We’ll omit background(), so it doesn’t get too crazy.)

image

(It generally isn’t a good idea to try to put everything into one diagram, and we’ll stop doing this now that we’ve seen this one example.)

What happens as you scrub the mouse across the canvas? If you look carefully, you may notice that the circles changes from black (on the left), to red (about 2/3 of the way to the right), and then stays the same color when it is positioned between 2/3 of the way from the left to the right, and 100% of the way from the left to the right.

Range and Domain

A note about range and domain. The range of mouseX is 0 (the smallest value it can have) to the canvas width (its largest possible value). [The largest value is actually canvas width - 1. And we can write this in code as width - 1. width is another variable that Processing declares.]

The domain of fill() is 0 to 255. [If you use colorMode() you can change this.] Processing happens to treat color values greater than 255 as though they were 255, so the color only changes until the mouse x position reaches 255; then it stays the same.

If we want to modify the range of mouseX to match the domain of fill(), we have two alternatives:

float red = map(mouseX, 0, width - 1, 0, 255);

float red = mouseX * 255.0 / (width - 1);

Follow either of these by:

fill(red, 0, 0);

The first of these uses the map() function to re-scale a value from one range (0…width-1) to another (0…255). The other uses arithmetic functions to do the same thing. You can think of them as fixing the red arrow so that the output from mouseX matches the input to fill(). (Question: Why didn’t we need to do that for circle()?)

image

You may have done this on the Arduino, to connect a sensor input (range = 0…1023) to an output (domain = 0…255).

Note: mouseX is multiplied by 255.0

Time and Animation

mouseX is a way of getting information from the outside world into your program, so that it can respond (do something different when the world changes), and interact (participate in a cycle of response and display). The specific feature of the outside world it reads is the position of the mouse or trackpad.

image

Another feature of the outside world is time, and the way to get information about it is to use the millis() function. This returns the number of milliseconds (thousands of a second) since the sketch started running.

Note that millis() is a function, while mouseX is a variable. The immediate consequence is that you have to use parentheses () after millis(); use mouseX without following it by (). The other difference is that, within a call to draw() , mouseX will have the same value each time that you access it. (Processing sets its value, and then calls draw().) millis() can return a different value each time it is called.

Let’s drive the size of the circle from millis():

void setup() {
  size(400, 400);
}

void draw() {
  background(220);
  noStroke();
  fill(255, 0, 0);
  float size = millis();
  circle(200, 200, size);
}
image

What happens? What do you expect to happen?

The circle starts at a diameter of zero, and quickly grows. 0.4 seconds after the sketch starts, millis() returns 400, and the circle extends to the top right and bottom left corners. 0.166 seconds later, it covers the entire screen. (Why 0.166 seconds? Hint: 0.4s+0.166s=0.566s=566ms0.4 \textrm{s} + 0.166 \textrm{s} = 0.566 \textrm{s} = 566 \textrm{ms} 0.4 s + 0.166 s = 0.566 s = 566 ms. 566 pixels=400 pixels×?566 \textrm{ pixels} = 400 \textrm{ pixels} \times ?)

Let’s slow this down. Edit the line float size = … to say:

float size = millis() / 10.;

Note: It’s a good habit to use floating point numbers (10.0, or just 10.) instead of integers (10) for division. This is because millis() / 10 is an integer operation (because both millis() and 10 are integers), that rounds the result so that it is an integer. For example, 9 / 10 equals 0; 11 / 10 equals 1. That won’t make much difference here, because a fractional pixel is barely noticeable. However, it can cause trouble in other sketches, causing things that you expect to move smoothly to jump instead.

Motion

In the sketches above, the circle grows indefinitely. What if we want it to grow and then shrink? The sin() function is a good way of creating periodic motion. Instead of using millis() / 10. directly, we will pass it through the sin() function. Since sin() returns a value in the range -1…1, use map() to map this to a range 10…100. 10 will be the circle’s smallest diameter, and 100 will be its largest.

void setup() {
  size(400, 400);
}

void draw() {
  background(220);
  noStroke();
  fill(255, 0, 0);
  float size = map(sin(millis() / 10.), -1, 1, 10, 100);
  circle(200, 200, size);
}

This works, but the circle pulses very quickly. Remember that the job of the / 10. in millis() / 10. is to slow down the animation (by pretending that time is moving more slowly). We can slow it down more by dividing by a bigger number:

void setup() {
  size(400, 400);
}

void draw() {
  background(220);
  noStroke();
  fill(255, 0, 0);
  float size = map(sin(millis() / 100.), -1, 1, 10, 100);
  circle(200, 200, size);
}

Now let’s animate the circle’s vertical position, by connecting it to the time as well. Now the circle looks as though it were bouncing. Notice the difference in the arguments to map().

void setup() {
  size(400, 400);
}

void draw() {
  background(220);
  noStroke();
  fill(255, 0, 0);
  float size = map(sin(millis() / 100.), -1, 1, 10, 100);
  float y = map(sin(millis() / 100.), -1, 1, 0, height);
  circle(200, y, size);
}

Drive the size and y position at different frequencies. They are both still based on time, but with a different “slow down” divisor. This creates a more interesting effect:

void setup() {
 size(400, 400);
}

void draw() {
  background(220);
  noStroke();
  fill(255, 0, 0);
  float size = map(sin(millis() / 100.), -1, 1, 10, 100);
  float y = map(sin(millis() / 150.), -1, 1, 0, height);
  circle(200, y, size);
}

Now let’s animate the horizontal position:

void setup() {
 size(400, 400);
}

void draw() {
  background(220);
  noStroke();
  fill(255, 0, 0);
  float size = map(sin(millis() / 100.), -1, 1, 10, 100);
  float x = map(sin(millis() / 150.), -1, 1, 0, width);
  float y = map(sin(millis() / 150.), -1, 1, 0, height);
  circle(x, y, size);
}

The circle moves back and forth along a diagonal. This is because x and y are set to values that vary in the same way (at the same frequency). Let’s use different frequencies for x and y. This produces a more interesting motion.

void setup() {
 size(400, 400);
}

void draw() {
  background(220);
  noStroke();
  fill(255, 0, 0);
  float size = map(sin(millis() / 100.), -1, 1, 10, 100);
  float x = map(sin(millis() / 175.), -1, 1, 0, width);
  float y = map(sin(millis() / 150.), -1, 1, 0, height);
  circle(x, y, size);
}

Remove the call to background() so that the canvas is never cleared. Every circle that has ever been drawn remains, with newer circles drawing on top of older ones. (We have also commented out the call to noStroke() to make it easier to see the different circles. You can try uncommenting this line to see what happens.)

void setup() {
  size(400, 400);
}

void draw() {
  //noStroke();
  fill(255, 0, 0);
  float size = map(sin(millis() / 100.), -1, 1, 10, 100);
  float x = map(sin(millis() / 175.), -1, 1, 0, width);
  float y = map(sin(millis() / 150.), -1, 1, 0, height);
  circle(x, y, size);
}

Outside reading: The shape that this creates is a Lissajous curve.

void setup() {
  size(400, 400);

}

void draw() {
  //noStroke();
	float hue 
  fill(255, 0, 0);
  float size = map(sin(millis() / 100.), -1, 1, 10, 100);
  float x = map(sin(millis() / 175.), -1, 1, 0, width);
  float y = map(sin(millis() / 150.), -1, 1, 0, height);
  circle(x, y, size);
}

Adding back colors:

void setup() {
  size(400, 400);
  colorMode(HSB);
}

void draw() {
  //noStroke();
  float hue = millis() / 10 % 360;
  fill(hue, 255, 255);
  float size = map(sin(millis() / 100.), -1, 1, 10, 100);
  float x = map(sin(millis() / 175.), -1, 1, 0, width);
  float y = map(sin(millis() / 150.), -1, 1, 0, height);
  circle(x, y, size);
}
image

Image Credits

Illustration icons made by Freepik from www.flaticon.com

©2020–2022 by Oliver Steele.