animation

The basic building blocks of computer animation are simple: draw an image, blank the screen, draw a slightly different image.

simple linear animation

Let's animate a square across the screen:

/* global variables */
int rectX = 0;
int rectY = 50;
int rectSize = 50;

/* built-in functions */
void setup() {
  size(300, 300);
  noStroke();
  smooth();
}

void draw() {
  background(0);
  moveSquare();
  drawSquare();
}

/* custom functions */
// moves the square one unit to the right
void moveSquare() {
  rectX = rectX + 1;
}

// draws the square
void drawSquare() {
  fill(255, 128, 0);  // orange
  rect(rectX, rectY, rectSize, rectSize);
}

A problem here is that the square moves off the right edge of the screen; let's rewrite it so that it wraps around to the left side:

// moves the square
void moveSquare() {
  if (rectX < width) {
    rectX = rectX + 1;
  } else {
    // reset to the left of the screen
    rectX = 0;
  }
}

Let's modify the last example to make the square move erratically across the canvas using random().

/* global variables */
int rectX;
int rectY;
int rectSize = 50;

/* built-in functions */
void setup() {
  size(300, 300);
  noStroke();
  smooth();

  resetSquare();
}

void draw() {
  background(0);
  moveSquare();
  drawSquare();
}

/* custom functions */
// resets the square's position
void resetSquare() {
  rectX = 0;
  rectY = 50; 
}

// moves the square erratically
void moveSquare() {
  if (rectX < width && rectY < height) {
    // random() returns a float, so we have to transform it into an integer;
    // this is done with casting, using the (int) operator.
    rectX = rectX+(int)random(0, 5);
    rectY = rectY+(int)random(0, 5);
  } else {
    resetSquare();
  }
}

// draws the square
void drawSquare() {
  fill(255, 128, 0);  // orange
  rect(rectX, rectY, rectSize, rectSize);
}

adding direction to the mix

We can add an extra variable which allows us to set the direction of movement. Let's modify the second example to make the square bounce from side to side.

/* global variables */
int rectX = 0;
int rectY = 50;
int rectSize = 50;
// we'll use an integer to represent direction:
// 1 -> left to right
// -1 -> right to left
int direction = 1;

/* built-in functions */
void setup() {
  size(300, 300);
  noStroke();
  smooth();
}

void draw() {
  background(0);
  moveSquare();
  drawSquare();
}

/* custom functions */
// moves the square
void moveSquare() {
  if (rectX > (width-rectSize)) {
    // the square is too far to the right, change direction to left
    direction = -1;
  } else if (rectX < 0) {
    // the square is too far to the left, change direction to right
    direction = 1;
  }

  rectX = rectX + direction;
}

// draws the square
void drawSquare() {
  fill(255, 128, 0);  // orange
  rect(rectX, rectY, rectSize, rectSize);
}

Note that along the x-axis, a positive direction translates as moving to the right and a negative direction results in moving towards the left. Similarly, along the y-axis, a positive direction means moving down and a negative direction means moving up.

We can apply the same concept to the erratic square example by using two variables for direction; one for the x and one for the y. Let's modify the third example to make the square bounce erratically all over the canvas.

/* global variables */
int rectX = 0;
int rectY = 50;
int rectSize = 50;
// we'll use an integer to represent direction:
// 1 -> left to right
// -1 -> right to left
int directionX = 1;
int directionY = 1;

/* built-in functions */
void setup() {
  size(300, 300);
  noStroke();
  smooth();
}

void draw() {
  background(0);
  moveSquare();
  drawSquare();
}

/* custom functions */
// moves the square erratically
void moveSquare() {
  if (rectX > (width-rectSize)) {
    // the square is too far to the right, change direction to left
    directionX = -1;
  } else if (rectX < 0) {
    // the square is too far to the left, change direction to right
    directionX = 1;
  }

  if (rectY > (height-rectSize)) {
    // the square is too far to the bottom, change direction to up
    directionY = -1;
  } else if (rectY < 0) {
    // the square is too far to the top, change direction to down
    directionY = 1;
  }

  int stepX = (int)random(5);
  int stepY = (int)random(5);

  rectX = rectX + (directionX*stepX);
  rectY = rectY + (directionY*stepY);
}

// draws the square
void drawSquare() {
  fill(255, 128, 0);  // orange
  rect(rectX, rectY, rectSize, rectSize);
}

easing

For all the examples up to now, the squares were moving by a constant amount every frame. This results in blocky and unnatural animation.

We can ease in and/or ease out the movement to fix this. Easing implies that the amount of change between each frame gets gradually greater or smaller.

Let's modify the example where the square bounces from side to side by making it ease out each movement. Instead of having it move by 1 pixel for each frame, we will tell the square to always move 1/10th of the way between its current position and the target position:

/* global variables */
int rectX = 0;
int rectY = 50;
int rectSize = 50;
int targetLeft;
int targetRight;
int currTarget;

/* built-in functions */
void setup() {
  size(300, 300);
  noStroke();
  smooth();

  targetLeft = 0;
  targetRight = width-rectSize;
  setTarget(targetLeft);
}

void draw() {
  background(0);
  moveSquare();
  drawSquare();
}

/* custom functions */
// sets the target x position of the square
void setTarget(int newTarget) {
  currTarget = newTarget; 
}

// moves the square
void moveSquare() {
  // find the distance from the target to the square
  int dX = currTarget-rectX;

  // ease towards the target
  int stepX = dX/10;

  // set the new square position
  rectX += stepX;

  // check if the square arrived at the target
  if (rectX <= targetLeft) {
    setTarget(targetRight);
  } else if (rectX >= targetRight) {
    setTarget(targetLeft); 
  }
}

// draws the square
void drawSquare() {
  fill(255, 128, 0);  // orange
  rect(rectX, rectY, rectSize, rectSize);
}					

If you try to run this sketch, you'll see that it doesn't quite work. The square moves to the right and suddenly stops. If you look closely, you'll notice that it never actually reaches the target. This is because int stepX = dX/10; is an int, and as soon as dX is less than 10, stepX becomes 0 and the square stops moving. So, if the square still has 9 units to go until it reaches the target, it will never reach it because it will move by 0 units. We need to set a minimum offset at which point we assume we are close enough to the target:

/* global variables */
// ...
// this value is used to calculate the distance of the step but
// also to determine the minimum offset to reach the target
int easeFactor = 10;

// ...

/* custom functions */
// ...
void moveSquare() {
  // find the distance from the target to the square
  int dX = currTarget-rectX;

  // ease towards the target
  int stepX = dX/easeFactor;

  // set the new square position
  rectX += stepX;

  // check if the step is small enough to consider it having arrived at the target
  if ((rectX-targetLeft) <= easeFactor) {
    setTarget(targetRight);
  } else if ((targetRight-rectX) <= easeFactor) {
    setTarget(targetLeft); 
  }
}

// ...

The example now works properly, although the square still never reaches its targets. We will come up with a more adequate solution in the next section.

animation & interaction

Let's add interaction to our animation by making a sketch where the square moves to wherever the mouse is clicked:

/* global variables */
int rectX = 0;
int rectY = 50;
int rectSize = 50;
// this value is used to calculate the distance of the step but
// also to determine the minimum offset to reach the target
int easeFactor = 10;
boolean goToTarget;
int targetX;
int targetY;

/* built-in functions */
void setup() {
  size(300, 300);
  rectMode(CENTER);
  noStroke();
  smooth();

  goToTarget = false;
}

void draw() {
  background(0);

  if (goToTarget) moveSquare();
  drawSquare();
}

void mousePressed() {
  targetX = mouseX;
  targetY = mouseY;

  goToTarget = true;
}

/* custom functions */
// moves the square
void moveSquare() {
  // find the distance from the mouse click to the square
  int dX = targetX-rectX;
  int dY = targetY-rectY;

  // ease towards the target
  int stepX = dX/easeFactor;
  int stepY = dY/easeFactor;

  // set the new square position
  rectX += stepX;
  rectY += stepY;

  // check if the distance is small enough to consider it having arrived at the target
  if (abs(dX) <= easeFactor && abs(dY) <= easeFactor) {
    goToTarget = false;
  }
}

// draws the square
void drawSquare() {
  fill(255, 128, 0);  // orange
  rect(rectX, rectY, rectSize, rectSize);
}

We have the same issue as with the previous sketch; the square does not line up perfectly with the target point, and it is a lot more noticeable in this case. Instead of hacking together a solution, let's fix the problem at its source, which is that we are dividing ints to get the amount of distance to travel each step. When we get to the point where the distance between the square and the target point is less than easeFactor, that division returns 0 and the square stops moving. We need to add precision to our calculations, and therefore rewrite the code using floats for our position values:

/* global variables */
float rectX = 0;
float rectY = 50;
int rectSize = 50;
// this value is used to calculate the distance of the step
int easeFactor = 10;
float acceptableOffset = 0.001;
boolean goToTarget;
float targetX;
float targetY;

// ...

/* custom functions */
// moves the square
void moveSquare() {
  // find the distance from the mouse click to the square
  float dX = targetX-rectX;
  float dY = targetY-rectY;
  
  // ease towards the target
  float stepX = dX/easeFactor;
  float stepY = dY/easeFactor;
  
  // set the new square position
  rectX += stepX;
  rectY += stepY;
  
  // check if the distance is small enough to consider it having arrived at the target
  if (abs(dX) <= acceptableOffset && abs(dY) <= acceptableOffset) {
    goToTarget = false;
  }
}

// ...

interpolating color

It is important to select an appropriate color space when interpolating color. HSB is generally more appropriate than RGB because each of the perceptual aspects of the color are broken apart; the shift in hue is separate from the shift in saturation and the shift in brightness. For example, the halfway point between orange (255, 128, 0) and light blue (0, 128, 255) in RGB space is grey (128, 128, 128), which does not feel quite right. On the other hand, RGB can be a better choice when the change in hue is significant. For example, to go from red to green in HSB space you would have to go through all the spectrum colors in between: red -> orange -> yellow -> green.

We will modify the interactive example above so that the square's color gradually changes from yellow to red as it moves towards its target. We will use the lerpColor() function to calculate the intermediate color values:

/* global variables */
float rectX = 0;
float rectY = 50;
int rectSize = 50;

// this value is used to calculate the distance of the step
int easeFactor = 10;
float acceptableOffset = 0.001;
boolean goToTarget;
float startX;
float startY;
float targetX;
float targetY;

color currColor;
color idleColor;
color activeColor;

/* built-in functions */
void setup() {
  size(300, 300);
  rectMode(CENTER);
  colorMode(HSB, 360, 100, 100);  // use the same ranges as the color selector
  noStroke();
  smooth();

  goToTarget = false;

  idleColor = color(54, 98, 98);   // yellow
  activeColor = color(2, 98, 97);  // red
}

void draw() {
  // motion blur
  fill(0, 30);
  rect(width/2, height/2, width, height);

  if (goToTarget) moveSquare();
  colorSquare();
  drawSquare();
}

void mousePressed() {
  // save the starting position
  startX = rectX;
  startY = rectY;
  // save the target position
  targetX = mouseX;
  targetY = mouseY;

  goToTarget = true;
}

/* custom functions */
// moves the square
void moveSquare() {
  // find the distance from the mouse click to the square
  float dX = targetX-rectX;
  float dY = targetY-rectY;

  // ease towards the target
  float stepX = dX/easeFactor;
  float stepY = dY/easeFactor;

  // set the new square position
  rectX += stepX;
  rectY += stepY;

  // check if the distance is small enough to consider it having arrived at the target
  if (abs(dX) <= acceptableOffset && abs(dY) <= acceptableOffset) {
    goToTarget = false;
  }
}

// calculates the color ratio based on position
void colorSquare() {
  float ratioX = (rectX-startX)/(targetX-startX);
  float ratioY = (rectY-startY)/(targetY-startY);
  float ratioAvg = (ratioX+ratioY)/2;
  currColor = lerpColor(idleColor, activeColor, ratioAvg);
}

// draws the square
void drawSquare() {
  fill(currColor);
  rect(rectX, rectY, rectSize, rectSize);
}

Processing Workshop

Elie Zananiri
Alberta College of Art + Design
3-5 April 2008