Animation and Randomness

Time-Based Animation

We can make the sketch animate by using time as an input. Modify y so that it is a function of time (millis()). This causes the sketch to animate.

function draw() {
	background(100);

	for (let i = 0; i < 20; i++) {
		let x = 10 + 50 * i;
		let y = map(sin(i + millis() / 100), -1, 1, 80, 120);
		circle(x, y, 20);
	}
}
image

Note: Now that the circles are drawn in a different position each time draw() is called, it is important to clear the canvas (background(200)) at the beginning of draw(). Otherwise each circle will be drawn on top of all the circles drawn by previous calls to draw(). (This has been happening in all the sketches so far, but since all the circles were drawn at the same position, this was not detectable.)

Try this: Change the sketch to animate the size of the object, instead of its position. You can base your work off of the previous two sketches.

Random

The previous sketch uses time to drive one of the shape’s properties (its vertical position). Let’s use random() instead.

function draw() {
	background(100);

	for (let i = 0; i < 20; i++) {
		let x = 10 + 50 * i;
    let y = random(80, 120);
		circle(x, y, 20);
	}
  noLoop();
}
image
image
image

Note: There are three screenshots next to the code, to show that the random() returns a different sequence of values, and therefore the sketch draws a different image, each time it is run.

The noLoop() function prevents draw() from being called again. Let’s see what would happen if we removed it.

function draw() {
	background(100);

	for (let i = 0; i < 20; i++) {
		let x = 10 + 50 * i;
    let y = random(80, 120);
		circle(x, y, 20);
	}
}
image

Aargh! Just as the previous sketch drew the circles at different positions each time it was run, this one draws them at different positions every frame.

What if we want to remember the position from frame to frame, so that we can draw the circles at the same position each time (or, move them in a more organized way that incorporates their previous position)?

In JavaScript, the way to remember things is to use a variable. A variable can hold a single value, but we want to remember several values: the vertical position of each circle. The trick is that the single value that the variables holds can be an array, which itself holds several values. Here’s what it looks like to use an array to set to hold the y values, inside draw().

function draw() {
	background(100);
	
	let ys = new Array();
	
	for (let i = 0; i < 20; i++) {
		ys[i] = random(80, 120);
	}

	for (let i = 0; i < 20; i++) {
		let x = 10 + 50 * i;
		circle(x, ys[i], 20);
	}
}
image

Note: You could also write let ys = [] instead of let ys = new Array(). [] is an abbreviation for new Array().

Advanced note: Each time an array item is assigned, as in ys[i] = random(80, 120), JavaScript makes the Array large enough to hold the new item. [Technically this is not quite true. But the effect is similar: you don’t need to worry about making an array large enough when you create it.] But you can also write new Array(20), if you know in advance how large the array needs to be.

As you can see above, simply using an array as intermediary doesn’t help with the jitter if the array is still initialized on each call to draw(). Each call to draw() makes a new array, stores it in a new variable named ys, and fills the array with a new set of random numbers.

Let’s make the array a global variable, and initialize it in setup() instead of draw().

let ys = new Array();	

function setup() {
	createCanvas(windowWidth, windowHeight);
	for (let i = 0; i < 20; i++) {
		ys[i] = random(80, 120);
	}
}

function draw() {
	background(100);
	
	for (let i = 0; i < 20; i++) {
		let x = 10 + 50 * i;
		circle(x, ys[i], 20);
	}
}
image

Now we can combine this with time-based animation, to get an effect that uses both randomness (from random() in setup()) and time (from millis() in draw()), without getting new random numbers every frame.

function draw() {
	background(100);
	
	for (let i = 0; i < 20; i++) {
		let x = 10 + 50 * i;
		let size = map(sin(i + millis() / 100), -1, 1, 10, 20);
		circle(x, ys[i], size);
	}
}
image

Update-Based (or Incremental) Animation

An alternative to time-based animation is to modify (or update) some values each time draw() is called.

The previous sketch initializes an array with random values, and then combines the values that it reads from this array with an expression that uses time. This sketch doesn’t use time at all; instead it writes to the array as well, so that the next frame can pick up where this frame left off.

let ys = new Array();	

function setup() {
	createCanvas(windowWidth, windowHeight);
	for (let i = 0; i < 20; i++) {
		ys[i] = random(80, 120);
	}
}

function draw() {
	background(100);
	
	for (let i = 0; i < 20; i++) {
		let x = 10 + 50 * i;
		circle(x, ys[i], 20);
		ys[i] += random(1);
	}
}
image

Noise

Randomness is good for organic or interesting patterns. Continuity and stability are necessary for smooth, pleasant animation. Storing the random values in an array is one way to combine randomness with continuity.

Another is to use the noise() function. Like random(), noise() returns values that don’t seem to have much to do with each other, at least when its arguments are far apart (as they are in the next sketch). Unlike random(), noise() returns the same values each time you call it with the same arguments.

Here’s the sketch from the Random section above, with random() replaced by noise(). Unlike the first sketch in Random, we don’t need noLoop() to keep the drawing from flickering: since for each circle noise() is called with the same input (i), its value is the same, and draw() draws the same thing from frame to frame.

function draw() {
	background(100);

	for (let i = 0; i < 20; i++) {
		let x = 10 + 50 * i;
    let y = map(noise(i), 0, 1, 80, 120);
		circle(x, y, 20);
	}
}
image

Note: noise() returns a value between 0 and 1, like random() with no arguments. To make it behave like random(80, 120), we must use map() or write our own version of this arithmetic.

noise() can take up to three values. We have used one: the index of the circle within the line of circles. Add a second argument to noise(), so that its output is based on the time, as well as on the circle’s index (i).

function draw() {
	background(100);

	for (let i = 0; i < 20; i++) {
		let x = 10 + 50 * i;
    let y = map(noise(i, millis() / 1000), 0, 1, 80, 120);
		circle(x, y, 20);
	}
}
image

noise() returns values that are close together when its inputs are close together. (It is kind of like a cross between random() and sine(). ) The successive values for i from item to item are far apart, so there’s not any continuity between the first and the second element. The successive values of millis() / 1000 are close together from one call to draw() to the next so there is continuity from frame to frame, resulting in a smooth animation. (This is why we divide millis() by 1000. The value of millis() itself changes too much from one frame to the next; this is how we slow it down.)

Theory: Sources of change

In order to draw a different image each frame, draw() needs access to something that has a different value each time draw() is called. Here are some possibilities:

  • Time. In p5.js, the frameCount function and the millis() variable are the sources of time. We explored that in Time-Based Animation.
  • A global variable. You can define your own variable that has global scope (is defined outside of setup() and draw()), and use it to remember changes from one frame to the next. There was an example of this in Update-Based Animation.
  • Randomness. The random() function returns a different number each time you use it. This will a frame that calls random() to draw differently each frame. There were an examples of this in Random and Noise.
  • Computer peripherals. Input devices that are connected to or built into the computer include the mouse, keyboard, webcam, and microphone.
  • Other connections to the outside world. These can come from an Arduino or other device connected on the USB serial port, or from the internet.

This page explored the first three of these.

©2020–2022 by Oliver Steele.