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 positionvoid 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 leftint 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 leftint 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 leftdirectionX = -1;} else if (rectX < 0) { // the square is too far to the left, change direction to rightdirectionX = 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 squarevoid 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 targetint easeFactor = 10;// ... /* custom functions */ // ... void moveSquare() { // find the distance from the target to the square int dX = currTarget-rectX; // ease towards the targetint stepX = dX/easeFactor;// set the new square position rectX += stepX; // check if the step is small enough to consider it having arrived at the targetif ((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 squareint 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 targetif (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 squarefloat dX = targetX-rectX; float dY = targetY-rectY;// ease towards the targetfloat 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 targetif (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 selectornoStroke(); smooth(); goToTarget = false;idleColor = color(54, 98, 98); // yellow activeColor = color(2, 98, 97); // red} void draw() { // motion blurfill(0, 30); rect(width/2, height/2, width, height);if (goToTarget) moveSquare();colorSquare();drawSquare(); } void mousePressed() { // save the starting positionstartX = 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 positionvoid 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); }