Chapter 3: Oscillation
Trigonometry is a sine of the times.
—Anonymous
Bridget Riley, a celebrated British artist, was a driving force behind the Op Art movement of the 1960s. Her work features geometric patterns that challenge the viewer’s perceptions and evoke feelings of movement or vibration. Her 1974 piece Gala showcases a series of curvilinear forms that ripple across the canvas, evoking the natural rhythm of the sine wave.
In Chapters 1 and 2, I carefully worked out an object-oriented structure to animate a shape in a p5.js canvas, using a vector to represent position, velocity, and acceleration driven by forces in the environment. I could move straight from here into topics such as particle systems, steering forces, group behaviors, and more. However, doing so would mean skipping a fundamental aspect of motion in the natural world: oscillation, or the back-and-forth movement of an object around a central point or position.
To model oscillation, you need to understand a little bit about trigonometry, the mathematics of triangles. Learning some trig will give you new tools to generate patterns and create new motion behaviors in a p5.js sketch. You’ll learn to harness angular velocity and acceleration to spin objects as they move. You’ll be able to use the sine and cosine functions to model nice ease-in, ease-out wave patterns. You’ll also learn to calculate the more complex forces at play in situations that involve angles, such as a pendulum swinging or a box sliding down an incline.
I’ll start with the basics of working with angles in p5.js, then cover several aspects of trigonometry. In the end, I’ll connect trigonometry with what you learned about forces in Chapter 2. This chapter’s content will pave the way for more sophisticated examples that require trig later in this book.
Angles
Before going any further, I need to make sure you understand how the concept of an angle fits into creative coding in p5.js. If you have experience with p5.js, you’ve undoubtedly encountered this issue while using the rotate()
function to rotate and spin objects. You’re most likely to be familiar with the concept of an angle as measured in degrees (see Figure 3.1).
A full rotation goes from 0 to 360 degrees, and 90 degrees (a right angle) is one-fourth of 360, shown in Figure 3.1 as two perpendicular lines.
Angles are commonly used in computer graphics to specify a rotation for a shape. For example, the square in Figure 3.2 is rotated 45 degrees around its center.
The catch is that, by default, p5.js measures angles not in degrees but in radians. This alternative unit of measurement is defined by the ratio of the length of the arc of a circle (a segment of the circle’s circumference) to the radius of that circle. One radian is the angle at which that ratio equals 1 (see Figure 3.3). A full circle (360 degrees) is equivalent to radians, 180 degrees is equivalent to radians, and 90 degrees is equivalent to radians.
The formula to convert from degrees to radians is as follows:
Thankfully, if you prefer to think of angles in degrees, you can call angleMode(DEGREES)
, or you can use the convenience function radians()
to convert values from degrees to radians. The constants PI
, TWO_PI
, and HALF_PI
are also available (equivalent to 180, 360, and 90 degrees, respectively). For example, here are two ways in p5.js to rotate a shape by 60 degrees:
let angle = 60;
rotate(radians(angle));
angleMode(DEGREES);
rotate(angle);
What Is Pi?
The mathematical constant pi (or the Greek letter ) is a real number defined as the ratio of a circle’s circumference (the distance around the outside of the circle) to its diameter (a straight line that passes through the circle’s center). It’s equal to approximately 3.14159 and can be accessed in p5.js with the built-in PI
variable.
While degrees can be useful, for the purposes of this book, I’ll be working with radians because they’re the standard unit of measurement across many programming languages and graphics environments. If they’re new to you, this is a good opportunity to practice! Additionally, if you aren’t familiar with the way rotation is implemented in p5.js, I recommend watching my Coding Train video series on transformations in p5.js.
Exercise 3.1
Rotate a baton-like object around its center by using translate()
and rotate()
.
Angular Motion
Another term for rotation is angular motion—that is, motion about an angle. Just as linear motion can be described in terms of velocity—the rate at which an object’s position changes over time—angular motion can be described in terms of angular velocity—the rate at which an object’s angle changes over time. By extension, angular acceleration describes changes in an object’s angular velocity.
Luckily, you already have all the math you need to understand angular motion. Remember the stuff I dedicated almost all of Chapters 1 and 2 to explaining?
You can apply exactly the same logic to a rotating object:
In fact, these angular motion formulas are simpler than their linear motion equivalents since the angle here is a scalar quantity (a single number), not a vector! This is because in 2D space, there’s one axis of rotation; in 3D space, the angle would become a vector. (Note that in most contexts, these formulas would include a multiplication by the change in time, referred to as delta time. I’m assuming a delta time of 1 that corresponds to one frame of animation in p5.js.)
Using the answer from Exercise 3.1, let’s say you wanted to rotate a baton in p5.js by a certain angle. Originally, the code might have read as follows:
translate(width / 2, height / 2);
rotate(angle);
line(-60, 0, 60, 0);
circle(60, 0, 16);
circle(-60, 0, 16, 16);
angle = angle + 0.1;
Adding in the principles of angular motion, I can instead write the following example (the solution to Exercise 3.1).
let angle = 0;
Position
let angleVelocity = 0;
Velocity
let angleAcceleration = 0.0001;
Acceleration
function setup() {
createCanvas(640, 240);
}
function draw() {
background(255);
translate(width / 2, height / 2);
rotate(angle);
Rotate according to that angle.
stroke(0);
fill(127);
line(-60, 0, 60, 0);
circle(60, 0, 16);
circle(-60, 0, 16);
angleVelocity += angleAcceleration;
Angular equivalent of velocity.add(acceleration)
angle += angleVelocity;
Angular equivalent of position.add(velocity)
}
Instead of incrementing angle
by a fixed amount to steadily rotate the baton, for every frame I add angleAcceleration
to angleVelocity
, then add angleVelocity
to angle
. As a result, the baton starts with no rotation and then spins faster and faster as the angular velocity accelerates.
Exercise 3.2
Add an interaction to the spinning baton. How can you control the acceleration with the mouse? Can you introduce the idea of drag, decreasing the angular velocity over time so the baton eventually comes to rest?
The logical next step is to incorporate this idea of angular motion into the Mover
class. First, I need to add some variables to the class’s constructor:
class Mover {
constructor() {
this.position = createVector();
this.velocity = createVector();
this.acceleration = createVector();
this.mass = 1.0;
this.angle = 0;
this.angleVelocity = 0;
this.angleAcceleration = 0;
Variables for angular motion
}
Then, in update()
, the mover’s position and angle are updated according to the algorithm I just demonstrated:
update() {
this.velocity.add(this.acceleration);
this.position.add(this.velocity);
Regular old-fashioned motion
this.angleVelocity += this.angleAcceleration;
this.angle += this.angleVelocity;
Newfangled angular motion
this.acceleration.mult(0);
}
Of course, for any of this to matter, I also need to rotate the object when drawing it in the show()
method. (I’ll add a line from the center to the edge of the circle so that rotation is visible. You could also draw the object as a shape other than a circle.)
show() {
stroke(0);
fill(175, 200);
push();
Use push()
to save the current state so the rotation of this shape doesn’t affect the rest of the world.
translate(this.position.x, this.position.y);
Set the origin at the shape’s position.
rotate(this.angle);
Rotate by the angle.
circle(0, 0, this.radius * 2);
line(0, 0, this.radius, 0);
pop();
Use pop()
to restore the previous state after rotation is complete.
}
At this point, if you were to actually go ahead and create a Mover
object, you wouldn’t see it
behave any differently. This is because the angular acceleration is initialized to zero (this.angleAcceleration = 0;
). For the object to rotate, it needs a nonzero acceleration! Certainly, one option is to hardcode a number in the constructor:
this.angleAcceleration = 0.01;
You can produce a more interesting result, however, by dynamically assigning an angular acceleration in the update()
method according to forces in the environment. This could be my cue to start researching the physics of angular acceleration based on the concepts of torque and moment of inertia, but at this stage, that level of simulation would be a bit of a rabbit hole. (I’ll cover modeling angular acceleration with a pendulum in more detail in “The Pendulum”, as well as look at how third-party physics libraries realistically model rotational motion in Chapter 6.)
Instead, a quick-and-dirty solution that yields creative results will suffice. A reasonable approach is to calculate angular acceleration as a function of the object’s linear acceleration, its rate of change of velocity along a path vector, as opposed to its rotation. Here’s an example:
this.angleAcceleration = this.acceleration.x;
Use the x-component of the object’s linear acceleration to calculate angular acceleration.
Yes, this is arbitrary, but it does do something. If the object is accelerating to the right, its angular rotation accelerates in a clockwise direction; acceleration to the left results in a counterclockwise rotation. Of course, it’s important to think about scale in this case. The value of the acceleration vector’s x
component might be too large, causing the object to spin in a way that looks ridiculous or unrealistic. You might even notice a visual illusion called the wagon wheel effect: an object appears to be rotating more slowly or even in the opposite direction because of the large changes between each frame of animation.
Dividing the x
component by a value, or perhaps constraining the angular velocity to a reasonable range, could really help. Here’s the entire update()
function with these tweaks added.
update() {
this.velocity.add(this.acceleration);
this.position.add(this.velocity);
this.angleAcceleration = this.acceleration.x / 10.0;
Calculate angular acceleration according to acceleration’s x-component.
this.angleVelocity += this.angleAcceleration;
this.angleVelocity = constrain(this.angleVelocity, -0.1, 0.1);
Use constrain()
to ensure that angular velocity doesn’t spin out of control.
this.angle += this.angleVelocity;
this.acceleration.mult(0);
}
Notice that I’ve used multiple strategies to keep the object from spinning out of control. First, I divide acceleration.x
by 10
before assigning it to angleAcceleration
. Then, for good measure, I also use constrain()
to confine angleVelocity
to the range (–0.1, 0.1)
.
Exercise 3.3
Step 1: Create a simulation of objects being shot out of a cannon. Each object should experience a sudden force when shot (just once) as well as gravity (always present).
Step 2: Add rotation to the object to model its spin as it’s shot from the cannon. How realistic can you make it look?
Trigonometry Functions
I think I’m ready to reveal the secret of trigonometry. I’ve discussed angles, I’ve spun a baton. Now it’s time for . . . wait for it . . . sohcahtoa. Yes, sohcahtoa! This seemingly nonsensical word is actually the foundation for much of computer graphics work. A basic understanding of trigonometry is essential if you want to calculate angles, figure out distances between points, and work with circles, arcs, or lines. And sohcahtoa is a mnemonic device (albeit a somewhat absurd one) for remembering the meanings of the trigonometric functions sine, cosine, and tangent. It references the sides of a right triangle, as shown in Figure 3.4.
Take one of the non-right angles in the triangle. The adjacent side is the one touching that angle, the opposite side is the one not touching that angle, and the hypotenuse is the side opposite the right angle. Sohcahtoa tells you how to calculate the angle’s trigonometric functions in terms of the lengths of these sides:
- soh:
- cah:
- toa:
Take a look at Figure 3.4 again. You don’t need to memorize it, but see if you feel comfortable with it. Try redrawing it yourself. Next, let’s look at it in a slightly different way (see Figure 3.5).
See how a right triangle is created from the vector ? The vector arrow is the hypotenuse, and the components of the vector (x and y) are the sides of the triangle. The angle is an additional means for specifying the vector’s direction (or heading). Viewed in this way, the trigonometric functions establish a relationship between the components of a vector and its direction + magnitude. As such, trigonometry will prove very useful throughout this book. To illustrate this, let’s look at an example that requires the tangent function.
Pointing in the Direction of Movement
Think all the way back to Example 1.10, which featured a Mover
object accelerating toward the mouse (Figure 3.6).
You might notice that almost all the shapes I’ve been drawing so far have been circles. This is convenient for several reasons, one of which is that using circles allows me to avoid the question of rotation. Rotate a circle and, well, it looks exactly the same. Nevertheless, there comes a time in all motion programmers’ lives when they want to move something around onscreen that isn’t shaped like a circle. Perhaps it’s an ant, or a car, or a spaceship. To look realistic, that object should point in its direction of movement.
When I say “point in its direction of movement,” what I really mean is “rotate according to its velocity vector.” Velocity is a vector, with an x- and y-component, but to rotate in p5.js, you need one number, an angle. Let’s look at the trigonometry diagram once more, this time focused on an object’s velocity vector (Figure 3.7).
The vector’s x- and y-components are related to its angle through the tangent function. Using the toa in sohcahtoa, I can write the relationship as follows:
The problem here is that while I know the x- and y-components of the velocity vector, I don’t know the angle of its direction. I have to solve for that angle. This is where another function known as the inverse tangent, or arctangent (arctan or atan, for short), comes in. (There are also inverse sine and inverse cosine functions, called arcsine and arccosine, respectively.)
If the tangent of value a equals value b, then the inverse tangent of b equals a. For example:
If | |
then |
See how one is the inverse of the other? This allows me to solve for the vector’s angle:
If | |
then |
Now that I have the formula, let’s see where it should go in the Mover
class’s show()
method to make the mover (now drawn as a rectangle) point in its direction of motion. Note that in p5.js, the function for inverse tangent is atan()
:
show() {
let angle = atan(this.velocity.y / this.velocity.x);
Solve for the angle by using atan()
.
stroke(0);
fill(175);
push();
rectMode(CENTER);
translate(this.position.x, this.position.y);
rotate(angle);
Rotate according to that angle.
rect(0, 0, 30, 10);
pop();
}
This code is pretty darn close and almost works. There’s a big problem, though. Consider the two velocity vectors depicted in Figure 3.8.
Though superficially similar, the two vectors point in quite different directions—opposite directions, in fact! In spite of this, look at what happens if I apply the inverse tangent formula to solve for the angle of each vector:
I get the same angle! That can’t be right, though, since the vectors are pointing in opposite directions. It turns out this is a pretty common problem in computer graphics. I could use atan()
along with conditional statements to account for positive/negative scenarios, but p5.js (along with most programming environments) has a helpful function called atan2()
that resolves the issue for me.
show() {
let angle = atan2(this.velocity.y, this.velocity.x);
Use atan2()
to account for all possible directions.
push();
rectMode(CENTER);
translate(this.position.x, this.position.y);
rotate(angle);
Rotate according to that angle.
rect(0, 0, 30, 10);
pop();
}
To simplify this even further, the p5.Vector
class provides a method called heading()
, which takes care of calling atan2()
and returns the 2D direction angle, in radians, for any p5.Vector
:
let angle = this.velocity.heading();
The easiest way to do this!
With heading()
, it turns out you don’t actually need to implement the trigonometry functions in your code, but understanding how they’re all working is still helpful.
Exercise 3.4
Create a simulation of a vehicle that you can drive around the screen by using the arrow keys: the left arrow accelerates the car to the left, and the right arrow accelerates to the right. The car should point in the direction in which it’s currently moving.
Polar vs. Cartesian Coordinates
Anytime you draw a shape in p5.js, you have to specify a pixel position, a set of x- and y-coordinates. These are known as Cartesian coordinates, named for René Descartes, the French mathematician who developed the ideas behind Cartesian space.
Another useful coordinate system, known as polar coordinates, describes a point in space as a distance from the origin (like the radius of a circle) and an angle of rotation around the origin (usually called , the Greek letter theta). Thinking in terms of vectors, a Cartesian coordinate describes a vector’s x- and y-components, whereas a polar coordinate describes a vector’s magnitude (length) and direction (angle).
When working in p5.js, you may find it more convenient to think in polar coordinates, especially when creating sketches that involve rotational or circular movements. However, p5.js’s drawing functions understand only (x, y) Cartesian coordinates. Happily for you, trigonometry holds the key to converting back and forth between polar and Cartesian (see Figure 3.9). This allows you to design with whatever coordinate system you have in mind, while always drawing using Cartesian coordinates.
For example, given a polar coordinate with a radius of 75 pixels and an angle () of 45 degrees (or radians), the Cartesian x and y can be computed as follows:
The functions for sine and cosine in p5.js are sin()
and cos()
, respectively. Each takes one argument, a number representing an angle in radians. These formulas can thus be coded as follows:
let r = 75;
let theta = PI / 4;
let x = r * cos(theta);
let y = r * sin(theta);
Convert from polar (r, theta) to Cartesian (x, y).
This type of conversion can be useful in certain applications. For instance, moving a shape along a circular path using Cartesian coordinates isn’t so easy. However, with polar coordinates, it’s simple: just increment the angle! Here’s how it’s done with global r
and theta
variables.
let r;
let theta;
function setup() {
createCanvas(640, 240);
r = height * 0.45;
theta = 0;
Initialize all values.
}
function draw() {
background(255);
translate(width / 2, height / 2);
Translate the origin point to the center of the screen.
let x = r * cos(theta);
let y = r * sin(theta);
Polar coordinates (r, theta) are converted to Cartesian (x, y) for use in the circle()
function.
fill(127);
stroke(0);
line(0, 0, x, y);
circle(x, y, 48);
theta += 0.02;
Increase the angle over time.
}
Polar-to-Cartesian conversion is common enough that p5.js includes a handy function to take care of it for you. It’s included as a static method of the p5.Vector
class called fromAngle()
. It takes an angle in radians and creates a unit vector in Cartesian space that points in the direction specified by the angle. Here’s how that would look in Example 3.4:
let position = p5.Vector.fromAngle(theta);
Create a unit vector pointing in the direction of an angle.
position.mult(r);
To complete polar-to-Cartesian conversion, scale position
by r
.
circle(position.x, position.y, 48);
Draw the circle by using the x- and y-components of the vector.
Are you amazed yet? I’ve demonstrated some pretty great uses of tangent (for finding the angle of a vector) and sine and cosine (for converting from polar to Cartesian coordinates). I could stop right here and be satisfied. But I’m not going to. This is only the beginning. As I’ll show you next, what sine and cosine can do for you goes beyond mathematical formulas and right triangles.
Exercise 3.5
Using Example 3.4 as a basis, draw a spiral path. Start in the center and move outward. Note that this can be done by changing only one line of code and adding one line of code!
Exercise 3.6
Simulate the spaceship in the game Asteroids. In case you aren’t familiar with Asteroids, here’s a brief description: A spaceship (represented as a triangle) floats in 2D space. The left arrow key turns the spaceship counterclockwise; the right arrow key turns it clockwise. The Z key applies a thrust force in the direction the spaceship is pointing.
Properties of Oscillation
Take a look at the graph of the sine function in Figure 3.10, where y = sin(x).
The output of the sine function is a smooth curve alternating between –1 and 1, also known as a sine wave. This behavior, a periodic movement between two points, is the oscillation I mentioned at the start of the chapter. Plucking a guitar string, swinging a pendulum, bouncing on a pogo stick—all are examples of oscillating motion that can be modeled using the sine function.
In a p5.js sketch, you can simulate oscillation by assigning the output of the sine function to an object’s position. I’ll begin with a basic scenario: I want a circle to oscillate between the left side and the right side of a canvas (Figure 3.11).
This pattern of oscillating back and forth around a central point is known as simple harmonic motion (or, to be fancier, the periodic sinusoidal oscillation of an object). The code to achieve it is remarkably simple, but before I get into it, I’d like to introduce some of the key terminology related to oscillation (and waves).
When a moving object exhibits simple harmonic motion, its position (in this case, the x-position) can be expressed as a function of time, with the following two elements:
- Amplitude: The distance from the center of motion to either extreme
- Period: The duration (time) for one complete cycle of motion
To understand these terms, look again at the graph of the sine function in Figure 3.10. The curve never rises above 1 or below –1 along the y-axis, so the sine function has an amplitude of 1. Meanwhile, the wave pattern of the curve repeats every units along the x-axis, so the sine function’s period is . (By convention, the units here are radians, since the input value to the sine function is customarily an angle measured in radians.)
So much for the amplitude and period of an abstract sine function, but what are amplitude and period in the p5.js world of an oscillating circle? Well, amplitude can be measured rather easily in pixels. For example, if the canvas is 200 pixels wide, I might choose to oscillate around the center of the canvas, going between 100 pixels right of center and 100 pixels left of center. In other words, the amplitude is 100 pixels.
let amplitude = 100;
The amplitude is measured in pixels.
The period is the amount of time for one complete cycle of an oscillation. However, in a p5.js sketch, what does time really mean? In theory, I could say I want the circle to oscillate every three seconds, then come up with an elaborate algorithm for moving the object according to real-world time, using millis()
to track the passage of milliseconds. For what I’m trying to accomplish here, however, real-world time isn’t necessary. The more useful measure of time in p5.js is the number of frames that have elapsed, available through the built-in frameCount
variable. Do I want the oscillating motion to repeat every 30 frames? Every 50 frames? For now, how about a period of 120 frames:
let period = 120;
The period is measured in frames (the unit of time for animation).
Once I have the amplitude and period, it’s time to write a formula to calculate the circle’s x-position as a function of time (the current frame count):
let x = amplitude * sin(TWO_PI * frameCount / period);
amplitude
and period
are my own variables; frameCount
is built into p5.js.
Think about what’s going on here. First, whatever value the sin()
function returns is multiplied by amplitude
. As you saw in Figure 3.10, the output of the sine function oscillates between –1 and 1. Multiplying that value by my chosen amplitude—call it a—gives me the desired result: a value that oscillates between –a and a. (This is also a place where you could use p5.js’s map()
function to map the output of sin()
to a custom range.)
Now, think about what’s inside the sin()
function:
TWO_PI * frameCount / period
What’s going on here? Start with what you know. I’ve explained that sine has a period of , meaning it will start at 0 and repeat at , , , and so on. If my desired period of oscillation is 120 frames, I want the circle to be in the same position when frameCount
is at 120 frames, 240 frames, 360 frames, and so on. Here, frameCount
is the only value changing over time; it starts at 0 and counts upward. Let’s take a look at what the formula yields as frameCount
increases.
frameCount | frameCount / period | TWO_PI * frameCount / period |
---|---|---|
0 | 0 | 0 |
60 | 0.5 | |
120 | 1 | |
240 | 2 | |
. . . | . . . | . . . |
Dividing frameCount
by period
tells me the number of cycles that have been completed. (Is the wave halfway through the first cycle? Have two cycles completed?) Multiplying that number by TWO_PI
, I get the desired result, an appropriate input to the sin()
function, since TWO_PI
is the value required for sine (or cosine) to complete one full cycle.
Putting it together, here’s an example that oscillates the x
position of a circle with an amplitude of 100 pixels and a period of 120 frames.
function setup() {
createCanvas(640, 240);
}
function draw() {
background(255);
let period = 120;
let amplitude = 200;
let x = amplitude * sin(TWO_PI * frameCount / period);
Calculate the horizontal position according to the formula for simple harmonic motion.
stroke(0);
fill(127);
translate(width / 2, height / 2);
line(0, 0, x, 0);
circle(x, 0, 48);
}
Before moving on, I would be remiss not to mention frequency, the number of cycles of an oscillation per time unit. Frequency is the inverse of the period—that is, 1 divided by the period. For example, if the period is 120 frames, only 1/120th of a cycle is completed in 1 frame, and so the frequency is 1/120. In Example 3.5, I chose to define the rate of oscillation in terms of the period, and therefore I didn’t need a variable for frequency. Sometimes, however, thinking in terms of frequency rather than period is more useful.
Exercise 3.7
Using the sine function, create a simulation of a weight (sometimes referred to as a bob) that hangs from a spring from the top of the window. Use the map()
function to calculate the vertical position of the bob. In