Pendulums and sine waves in P5.js
I recently created some very cool art using simulated pendulums and sine waves!
A simple pendulum
My initial pendulum simulation was helped along by The Coding Train's video of a simple pendulum simulation.
Of course, if you're like me and understand better by reading, you can also read the pendulum section on The Nature of Code, which is also written by Daniel Shiffman from The Coding Train.
let pendulum;
let bg = '#000000';
function setup() {
createCanvas(window.innerWidth, window.innerHeight);
background(bg);
let len = height / 2;
pendulum = new Pendulum(len);
}
function draw() {
background(bg);
pendulum.update();
}
class Pendulum {
constructor(len) {
this.len = len;
this.angle = PI / 4;
this.angleV = 0; // angular velocity
this.angleA = 0; // angular acceleration
this.bob = createVector();
this.origin = createVector(width / 2, 0);
this.gravity = 0.1;
}
update() {
// Calculate force, acceleration, velocity, angle
const force = this.gravity * sin(this.angle);
this.angleA = (-1 * force) / this.len;
this.angleV += this.angleA;
this.angle += this.angleV;
// Calculate position of bob each frame
this.bob.x = this.len * sin(this.angle) + this.origin.x
this.bob.y = this.len * cos(this.angle) + this.origin.y;
// Draw string
stroke(color(255, 255, 255, 100));
strokeWeight(1);
line(this.origin.x, this.origin.y, this.bob.x, this.bob.y);
// Draw pendulum bob
stroke(color(255, 255, 255, 255));
strokeWeight(20);
noFill();
point(this.bob.x, this.bob.y);
}
}
From looking at the code, you can see that I've created a class that constructs a new pendulum on setup, then draws the update to the pendulum every frame.
Multiple pendulums
Adding multiple pendulums requires creating a loop that creates pendulums with different string lengths and pushes them to an array. On each call of the draw function, it loops through the pendulums and updates each of them.
let max_pendulums = 20;
let pendulums = [];
let bg = '#000000';
function setup() {
createCanvas(1920, 1080);
background(bg);
let len = 100;
for (let i = 0; i < max_pendulums; i++) {
pendulums.push(new Pendulum(len));
len += (height - 100) / max_pendulums;
}
}
function draw() {
background(bg);
for (let pendulum of pendulums) {
pendulum.update();
}
}
class Pendulum {
...
}
Bouncing pendulums
Next, I want to make these pendulums bouncy. This is done by keeping track of a global time variable, and getting the sine of that time. Then we multiply the sign by the height of the wave and add that to the base length of the pendulum string.
The general formula for this is: pendulum.length + sin(time) * height of bounce
After calculating the amount of bounce, that value is used in the this.bob.x
and this.bob.y
calculations. This makes the pendulums appear bouncy as they sway.
let time = 0;
function setup() {
...
}
function draw() {
time += 0.1;
...
}
class Pendulum {
constructor(len) {
...
}
update() {
// Lengthen and shorten the pendulum's string with a sine wave
let len = this.len + sin(time) * (height / (max_pendulums * 2.5));
// Calculate force, acceleration, velocity, angle
...
// Calculate position of bob each frame
this.bob.x = len * sin(this.angle) + this.origin.x
this.bob.y = len * cos(this.angle) + this.origin.y;
// Draw string
...
// Draw pendulum bob
...
}
}
Drawing waves
Now, to draw the rest of the owl. To create the cool rainbow effects (I think they look like skeins of embroidery floss), we need to remove the string and the "bob" from the pendulum, and draw a line from the last position of the pendulum to the current position.
I'm also going to use the lerpColor() function from P5.js to smoothly transition between colors. I'm doing this in the HSL color space because it creates a more attractive, vibrant gradient than RGB. The color is added to the pendulum class as a property, and is passed in as an argument when the pendulum is created.
You'll also note that I've removed the background method from the draw function. We want the lines to persist, not be wiped away with every call to draw().
Once you have the basic mechanics in place, you can let your creativity go wild!
function setup() {
createCanvas(3840, 2160);
colorMode(HSL, 255);
background(bg);
let col1 = '#ffbe6f';
let col2 = '#b894ff';
let len = 10;
for (let i = 0; i < max_pendulums; i++) {
let col = lerpColor(color(col1), color(col2), i / max_pendulums);
pendulums.push(new Pendulum(len, col));
len += (height - 210) / max_pendulums;
stopframe += 100 * (i + 1);
}
}
function draw() {
time += 0.3;
for (let pendulum of pendulums) {
pendulum.update();
}
}
class Pendulum {
constructor(len, col) {
this.angle = PI / 2; // PI / 2 makes a semicircle!
...
this.last = createVector();
this.col = col;
}
update() {
// Lengthen and shorten the pendulum's string with a sine wave
let len = this.len + sin(time) * (height / (max_pendulums * 2.5));
// Calculate force, acceleration, velocity, angle
...
// Calculate position of bob each frame
...
// If this is the first frame, set the last position
// of the pendulum bob to the current position
if (this.last.x === 0) {
this.last.x = this.bob.x;
this.last.y = this.bob.y;
}
stroke(this.col);
strokeWeight(1);
noFill();
line(this.last.x, this.last.y, this.bob.x, this.bob.y);
// Update the last position to the current position
this.last.x = this.bob.x;
this.last.y = this.bob.y;
}
}
Variations
By changing some of the variables, it's possible to get a wide variety of different effects. Reducing the amount that the time increases widens the sine wave. Multiplying the force by 1 instead of -1 will make the pendulum defy gravity.
You can check out the code for these variations on my GitHub.