Perfect loops in Processing

There are many possible ways to create looping animations in Processing, but my favourite uses the frameCount system variable. Starting from 0, frameCount increments after each frame is drawn. frameCount is global, and managed by Processing, so it can be queried from anywhere.

For example, if you wanted a rectangle to move 10px each frame, frameCount can be multiplied by 10 to find the position. The modulo operator can then be used to wrap the value to the screen width, so it loops back to the start after hitting the edge.

void setup() {
  size(704, 90, P2D);
  frameRate(30);
}

void draw() {
  background(0);
  rect(frameCount * 10 % width, 30, 30, 30);
}

Normalised values #

For complex animation, working with normalised values (0 to 1) keeps value ranges consistent and makes composing easier.

The timeLoop function below uses frameCount and a total duration in frames to increase smoothly from 0 and 1, then jumps back to 0. Because the result has a maximum of 1, it can be multiplied by the distance we want it to travel (in this case, the full width of the view).

void setup() {
  size(704, 90, P2D);
  frameRate(30);
}

void draw() {
  background(0);
  rect(timeLoop(60) * width, 30, 30, 30);
}

float timeLoop(float totalframes) {
  return frameCount % totalframes / totalframes;
}

Time offset #

Currently, the loop start point is very obvious and jarring. One way to create some interest and make the loop point more pleasing is to add lots of elements with their timing out of sync. Adding an offset parameter to the timeLoop function can give us that control.

void setup() {
  size(704, 150, P2D);
  frameRate(30);
}

void draw() {
  background(0);
  rect(timeLoop(60, 0) * width, 30, 30, 30);
  rect(timeLoop(60, 20) * width, 60, 30, 30);
  rect(timeLoop(60, 40) * width, 90, 30, 30);
}

float timeLoop(float totalframes, float offset) {
  return (frameCount + offset) % totalframes / totalframes;
}

Using the methods above, 16 bars that animate out of sync can be created. Each bar grows to the full height of 100px, then resets to 0.

void setup() {
  size(704, 200, P2D);
  frameRate(30);
  noStroke();
}

void draw() {
  background(0);
  for (float i = 0; i < 1; i += 1 / 16.0) {
    float barheight = timeLoop(60, i * 60) * 100;
    rect(36 + i * 640, 150 - barheight, 32, barheight);
  }
}

float timeLoop(float totalframes, float offset) {
  return (frameCount + offset) % totalframes / totalframes;
}

Easing curves #

All the animation so far has moved in a linear fashion — for each frame, the distance moved is the same. Linear timing is very robotic and not particularly graceful.

Given we’re working with normalised values, easing curves can be applied. The result of timeLoop just needs to be passed through the curve needed. The function below converts a sawtooth waveform (linear timing) into a triangle wave. That will make our bars go and up down, rather than just moving up.

float tri(float t) {
  return t < 0.5 ? t * 2 : 2 - (t * 2);
}

The function below converts a sawtooth waveform (linear timing) into the first quarter of a sine wave. Don’t worry if the maths involved doesn’t make sense — that’s a discussion for another time, and not important right now.

float inOutSin(float t) {
  return 0.5 - cos(PI * t) / 2;
}

Given timeLoop and the easing functions are all normalised, they can be composed in any order. The change below takes the result from timeLoop, makes it a triangle wave, then gives it ease-in-out sine timing. Here’s the full source code with those changes made.

void setup() {
  size(704, 200, P2D);
  frameRate(30);
  noStroke();
}

void draw() {
  background(0);
  for (float i = 0; i < 1; i += 1 / 16.0) {
    float barheight = inOutSin(tri(timeLoop(60, i * 60))) * 100;
    rect(36 + i * 640, 150 - barheight, 32, barheight);
  }
}

float timeLoop(float totalframes, float offset) {
  return (frameCount + offset) % totalframes / totalframes;
}

float tri(float t) {
  return t < 0.5 ? t * 2 : 2 - (t * 2);
}

float inOutSin(float t) {
  return 0.5 - cos(PI * t) / 2;
}

If we add another two duplicates of the bars, add some colour, and set the blend mode for drawing to “add”, things start to get interesting.

void draw() {
  background(0);
  blendMode(ADD);
  float barheight = 0;
  for (float i = 0; i < 1; i += 1 / 16.0) {
    fill(#ff0000);
    barheight = inOutSin(tri(timeLoop(60, i * 60))) * 100;
    rect(36 + i * 640, 150 - barheight, 32, barheight);
    fill(#00ff00);
    barheight = inOutSin(tri(timeLoop(60, i * 60 + 20))) * 100;
    rect(36 + i * 640, 150 - barheight, 32, barheight);
    fill(#0000ff);
    barheight = inOutSin(tri(timeLoop(60, i * 60 + 40))) * 100;
    rect(36 + i * 640, 150 - barheight, 32, barheight);
  }
}

Almost all the GIFs I’ve created in Processing use the timeLoop function above and the methods discussed in this article.

Published 27 August 2019.