Pendulum Wave Symphony - xelsed.ai

This sketch creates a mesmerizing pendulum wave visualization with 16 colorful balls swinging at slightly different periods. All pendulums start in sync, then gradually drift out of phase over 20 seconds, creating beautiful wave patterns before realigning. The physics-based motion uses cosine easing to simulate natural pendulum behavior.

๐ŸŽ“ Concepts You'll Learn

Animation with time-based motionTrigonometry and polar coordinatesClass-based object designArray iterationHSB color modeResponsive canvas sizingPhysics simulationPeriodic motionPhase relationships

๐Ÿ”„ Code Flow

Code flow showing setup, initpendulums, draw, windowresized, constructor, update, display

๐Ÿ’ก Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> initpendulums[initpendulums] setup --> draw[draw loop] draw --> background-clear[Background Clear] draw --> time-conversion[Time Conversion] draw --> pendulum-update-loop[Pendulum Update and Display Loop] pendulum-update-loop --> phase-calculation[Phase Calculation] pendulum-update-loop --> angle-calculation[Angle Calculation] pendulum-update-loop --> position-calculation[Position Calculation] pendulum-update-loop --> ball-drawing[Ball Drawing] pendulum-update-loop --> glow-drawing[Glow Effect Drawing] click setup href "#fn-setup" click initpendulums href "#fn-initpendulums" click draw href "#fn-draw" click background-clear href "#sub-background-clear" click time-conversion href "#sub-time-conversion" click pendulum-update-loop href "#sub-pendulum-update-loop" click phase-calculation href "#sub-phase-calculation" click angle-calculation href "#sub-angle-calculation" click position-calculation href "#sub-position-calculation" click ball-drawing href "#sub-ball-drawing" click glow-drawing href "#sub-glow-drawing"

๐Ÿ“ Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes the canvas, sets up the color system, and creates all the pendulum objects that will animate throughout the sketch.

function setup() {
  createCanvas(windowWidth, windowHeight);
  colorMode(HSB, 360, 100, 100, 100);
  maxAngle = radians(MAX_ANGLE_DEG);
  initPendulums();
}

Line by Line:

createCanvas(windowWidth, windowHeight)
Creates a canvas that fills the entire browser window, making the sketch responsive to window size
colorMode(HSB, 360, 100, 100, 100)
Switches from RGB to HSB color mode where hue ranges 0-360, saturation 0-100, brightness 0-100, and alpha 0-100. This makes rainbow colors easier to create
maxAngle = radians(MAX_ANGLE_DEG)
Converts the maximum swing angle from degrees (30) to radians, which is required for trigonometric functions like sin() and cos()
initPendulums()
Calls the initialization function to create all 16 pendulum objects with their calculated periods and lengths

initPendulums()

This function sets up the physics and layout of all pendulums. The key insight is that each pendulum has a slightly different period, causing them to drift in and out of sync. The period calculation ensures that after 20 seconds, all pendulums roughly realign. Lengths are mapped to periods using Tยฒ โˆ L, which matches real pendulum physics.

function initPendulums() {
  pendulums = [];

  const periods = [];
  for (let i = 0; i < NUM_PENDULUMS; i++) {
    const swings = MIN_SWINGS + i;
    const period = CYCLE_TIME / swings;
    periods.push(period);
  }

  const minPeriod = Math.min(...periods);
  const maxPeriod = Math.max(...periods);

  const minLen = height * 0.25;
  const maxLen = height * 0.70;

  const padX = width * 0.08;
  const spacingX = (width - 2 * padX) / (NUM_PENDULUMS - 1);
  const originY = height * 0.12;

  for (let i = 0; i < NUM_PENDULUMS; i++) {
    const period = periods[i];
    const length = map(
      period * period,
      minPeriod * minPeriod,
      maxPeriod * maxPeriod,
      minLen,
      maxLen
    );

    const originX = padX + i * spacingX;
    const hue = map(i, 0, NUM_PENDULUMS - 1, 0, 300);

    pendulums.push(new Pendulum(originX, originY, length, period, hue));
  }
}

๐Ÿ”ง Subcomponents:

for-loop Period Calculation Loop for (let i = 0; i < NUM_PENDULUMS; i++) { const swings = MIN_SWINGS + i; const period = CYCLE_TIME / swings; periods.push(period); }

Calculates a unique period for each pendulum so they swing at different speeds. The first pendulum completes 20 swings in 20 seconds, the second completes 21 swings, etc.

calculation Length to Period Mapping const length = map(period * period, minPeriod * minPeriod, maxPeriod * maxPeriod, minLen, maxLen)

Maps pendulum lengths proportionally to period squared (Tยฒ โˆ L), which matches real physics where longer pendulums swing slower

for-loop Pendulum Creation Loop for (let i = 0; i < NUM_PENDULUMS; i++) { ... pendulums.push(new Pendulum(...)); }

Creates 16 Pendulum objects with calculated periods, lengths, and rainbow-mapped hue values, then stores them in the pendulums array

Line by Line:

pendulums = []
Clears the pendulums array, removing any old pendulums before creating new ones (important when window is resized)
const periods = []
Creates an empty array to store the calculated period (swing time) for each pendulum
const swings = MIN_SWINGS + i
Each pendulum swings one more time than the previous: pendulum 0 swings 20 times, pendulum 1 swings 21 times, etc.
const period = CYCLE_TIME / swings
Calculates how long one complete swing takes. If a pendulum does 20 swings in 20 seconds, each swing takes 1 second
const minPeriod = Math.min(...periods)
Finds the shortest period (fastest pendulum) using the spread operator to pass array elements as arguments
const maxPeriod = Math.max(...periods)
Finds the longest period (slowest pendulum) to use as a range for mapping lengths
const minLen = height * 0.25
Sets the shortest pendulum length to 25% of canvas height
const maxLen = height * 0.70
Sets the longest pendulum length to 70% of canvas height
const padX = width * 0.08
Creates an 8% horizontal padding on each side so pendulums don't swing off the screen edges
const spacingX = (width - 2 * padX) / (NUM_PENDULUMS - 1)
Calculates equal horizontal spacing between pendulum attachment points across the canvas width
const originY = height * 0.12
Places all pendulum attachment points 12% down from the top of the canvas
const originX = padX + i * spacingX
Calculates the x position for each pendulum, spacing them evenly from left to right
const hue = map(i, 0, NUM_PENDULUMS - 1, 0, 300)
Maps pendulum index to hue values: first pendulum is red (0ยฐ), last is magenta (300ยฐ), creating a rainbow effect
pendulums.push(new Pendulum(originX, originY, length, period, hue))
Creates a new Pendulum object with calculated properties and adds it to the pendulums array

draw()

draw() runs 60 times per second. Each frame, it clears the canvas, gets the current time, and updates and draws all pendulums. The time-based approach (using millis()) means the animation will be consistent regardless of frame rate.

function draw() {
  background(230, 40, 5);

  const t = millis() / 1000;

  for (const p of pendulums) {
    p.update(t);
    p.display();
  }
}

๐Ÿ”ง Subcomponents:

calculation Background Clear background(230, 40, 5)

Clears the canvas each frame with a near-black color, preventing motion trails and creating a clean animation

calculation Time Conversion const t = millis() / 1000

Converts milliseconds since sketch start to seconds for easier period calculations

for-loop Pendulum Update and Display Loop for (const p of pendulums) { p.update(t); p.display(); }

Updates each pendulum's angle based on elapsed time, then draws it to the canvas

Line by Line:

background(230, 40, 5)
Fills the entire canvas with HSB color (hue=230, saturation=40, brightness=5), creating a very dark background with a slight blue tint
const t = millis() / 1000
Gets the number of milliseconds since the sketch started and divides by 1000 to convert to seconds
for (const p of pendulums)
Loops through each Pendulum object in the pendulums array using a for-of loop
p.update(t)
Calls the update method on the current pendulum, passing the elapsed time so it can calculate its current angle
p.display()
Calls the display method on the current pendulum to draw it at its current position

windowResized()

This function is called automatically by p5.js whenever the browser window is resized. It ensures the sketch remains responsive and properly scaled to fill the window at all times.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  initPendulums();
}

Line by Line:

resizeCanvas(windowWidth, windowHeight)
Resizes the p5.js canvas to match the current browser window dimensions
initPendulums()
Recalculates and recreates all pendulums with new positions and sizes based on the new canvas dimensions

Pendulum constructor(x, y, length, period, hue)

The constructor is called when creating a new Pendulum object. It stores all the properties needed to animate and draw this specific pendulum. Using createVector for the origin makes it easy to work with 2D coordinates.

constructor(x, y, length, period, hue) {
  this.origin = createVector(x, y);
  this.length = length;
  this.period = period;
  this.hue = hue;

  this.maxAngle = maxAngle;
  this.angle = 0;

  this.radius = min(width, height) * 0.02;
}

Line by Line:

this.origin = createVector(x, y)
Creates a p5.Vector to store the attachment point (pivot) of the pendulum at coordinates (x, y)
this.length = length
Stores the length of the pendulum string/rod in pixels
this.period = period
Stores how many seconds it takes for one complete swing cycle
this.hue = hue
Stores the color hue (0-360) for this pendulum's ball
this.maxAngle = maxAngle
Stores the maximum angle (in radians) this pendulum will swing to from vertical
this.angle = 0
Initializes the current angle to 0 (vertical position)
this.radius = min(width, height) * 0.02
Calculates the ball radius as 2% of the smaller canvas dimension, ensuring it scales with window size

update(t)

This is the heart of the animation. The cosine function creates natural pendulum motion because it smoothly eases in and out at the extremes (like a real pendulum). Each pendulum has a different period, so their phase values advance at different rates, causing the wave pattern effect.

update(t) {
  const phase = (TWO_PI * t) / this.period;
  this.angle = this.maxAngle * cos(phase);
}

๐Ÿ”ง Subcomponents:

calculation Phase Calculation const phase = (TWO_PI * t) / this.period

Converts elapsed time into a phase angle (in radians) that repeats every period

calculation Angle from Cosine this.angle = this.maxAngle * cos(phase)

Uses cosine to create smooth, natural pendulum motion that starts at maximum angle and eases to center

Line by Line:

const phase = (TWO_PI * t) / this.period
Calculates the current phase of oscillation. TWO_PI (2ฯ€) radians = one complete cycle. Dividing elapsed time by period gives how many cycles have completed, then multiplying by 2ฯ€ converts to radians
this.angle = this.maxAngle * cos(phase)
Uses cosine to calculate the current angle. cos() oscillates smoothly between -1 and 1, so multiplying by maxAngle gives smooth swinging motion. At t=0, cos(0)=1, so angle starts at maximum

display()

This function draws each pendulum. The key is converting the angle and length into x,y coordinates using polar-to-Cartesian conversion (sin for x, cos for y). The glow effect is created by drawing a larger, semi-transparent circle outline, making the animation more visually appealing.

display() {
  const x = this.origin.x + this.length * sin(this.angle);
  const y = this.origin.y + this.length * cos(this.angle);

  noStroke();
  fill(this.hue, 80, 100, 100);
  circle(x, y, this.radius * 2);

  stroke(this.hue, 80, 100, 40);
  strokeWeight(this.radius * 0.6);
  noFill();
  circle(x, y, this.radius * 2.8);
}

๐Ÿ”ง Subcomponents:

calculation Ball Position from Polar Coordinates const x = this.origin.x + this.length * sin(this.angle); const y = this.origin.y + this.length * cos(this.angle)

Converts the angle and length into x,y coordinates using sine and cosine, placing the ball at the end of the pendulum

calculation Main Ball Drawing noStroke(); fill(this.hue, 80, 100, 100); circle(x, y, this.radius * 2)

Draws a solid, fully opaque colored circle representing the pendulum bob

calculation Glow Effect Drawing stroke(this.hue, 80, 100, 40); strokeWeight(this.radius * 0.6); noFill(); circle(x, y, this.radius * 2.8)

Draws a larger, semi-transparent circle outline around the ball to create a glowing effect

Line by Line:

const x = this.origin.x + this.length * sin(this.angle)
Calculates the x position of the ball using sine. sin(angle) gives horizontal displacement, multiplied by length gives how far to move from the origin
const y = this.origin.y + this.length * cos(this.angle)
Calculates the y position using cosine. cos(angle) gives vertical displacement. Note: cos is used for y because at angle=0 (vertical), cos(0)=1 gives full length downward
noStroke()
Disables stroke (outline) drawing for the next shape
fill(this.hue, 80, 100, 100)
Sets the fill color to the pendulum's hue with high saturation (80) and brightness (100), creating a vivid color with full opacity
circle(x, y, this.radius * 2)
Draws a solid circle at the ball's position with diameter = radius * 2
stroke(this.hue, 80, 100, 40)
Sets the stroke color to the same hue but with only 40% opacity (alpha=40), making it semi-transparent
strokeWeight(this.radius * 0.6)
Sets the stroke thickness to 60% of the ball radius, creating a thin outline
noFill()
Disables fill for the next shape so only the outline is drawn
circle(x, y, this.radius * 2.8)
Draws a larger circle outline (diameter = radius * 2.8) around the ball, creating the glowing halo effect

๐Ÿ“ฆ Key Variables

pendulums array

Stores all 16 Pendulum objects. Used to update and draw each pendulum every frame

let pendulums = [];
NUM_PENDULUMS number

Constant that defines how many pendulums to create (16 in this sketch)

const NUM_PENDULUMS = 16;
CYCLE_TIME number

Constant defining how many seconds until all pendulums roughly realign (20 seconds)

const CYCLE_TIME = 20;
MIN_SWINGS number

Constant for how many swings the first (fastest) pendulum completes in one cycle (20)

const MIN_SWINGS = 20;
MAX_ANGLE_DEG number

Constant for the maximum swing angle in degrees (30ยฐ) before conversion to radians

const MAX_ANGLE_DEG = 30;
maxAngle number

Stores the maximum swing angle in radians (converted from MAX_ANGLE_DEG in setup). Used by all Pendulum objects

let maxAngle;
origin p5.Vector

Stores the attachment point (pivot) of each pendulum as an x,y coordinate

this.origin = createVector(x, y);
length number

Stores the length of each pendulum's string/rod in pixels

this.length = length;
period number

Stores how many seconds one complete swing cycle takes for each pendulum

this.period = period;
hue number

Stores the color hue (0-360 in HSB mode) for each pendulum's ball

this.hue = hue;
angle number

Stores the current swing angle (in radians) of each pendulum, updated every frame

this.angle = 0;
radius number

Stores the radius of each pendulum's ball, scaled to 2% of the smaller canvas dimension

this.radius = min(width, height) * 0.02;

๐Ÿงช Try This!

Experiment with the code by making these changes:

  1. Change NUM_PENDULUMS from 16 to 8 or 24 to see how fewer/more pendulums affect the wave pattern. Fewer pendulums create simpler patterns, more create complex ones.
  2. Modify CYCLE_TIME from 20 to 10 or 30 seconds to speed up or slow down how quickly the pendulums drift in and out of phase with each other.
  3. Change MAX_ANGLE_DEG from 30 to 45 or 60 to make the pendulums swing wider. Notice how this affects the visual impact without changing the timing.
  4. In the display() function, uncomment the three lines that draw the string (stroke, strokeWeight, line) to make the pendulum strings visible. Try different stroke colors or weights.
  5. Modify the hue mapping in initPendulums() from 'map(i, 0, NUM_PENDULUMS - 1, 0, 300)' to 'map(i, 0, NUM_PENDULUMS - 1, 0, 360)' to use the full color spectrum instead of just red to magenta.
  6. Change the background color from 'background(230, 40, 5)' to 'background(0, 0, 5)' for pure black, or try 'background(200, 50, 20)' for a different tint.
  7. In the Pendulum constructor, change 'this.radius = min(width, height) * 0.02' to 'this.radius = min(width, height) * 0.04' to make all balls twice as large.
Open in Editor & Experiment โ†’

๐Ÿ”ง Potential Improvements

Here are some ways this code could be enhanced:

PERFORMANCE initPendulums() - period calculation

The periods array is created and populated, then Math.min/max are called on it. This works but creates an intermediate array that isn't strictly necessary.

๐Ÿ’ก Calculate minPeriod and maxPeriod in a single loop while building the periods array, eliminating the need to iterate twice and use the spread operator.

STYLE display() method

The commented-out string drawing code takes up space and might confuse beginners about whether strings should be visible.

๐Ÿ’ก Move the commented code to a separate optional function like 'displayString()' that can be called conditionally, or document it better with a clear comment explaining how to enable it.

BUG Pendulum class - radius calculation

The radius is calculated in the constructor but uses the global width and height variables. If the window is resized, the canvas resizes but the radius values of existing Pendulum objects don't update.

๐Ÿ’ก Either recalculate radius in the update() method, or move the radius calculation to a getter method that always uses current canvas dimensions, or ensure initPendulums() is called on every resize (which it is, so this is actually handled correctly).

FEATURE Sketch overall

There's no way for users to interact with or control the animation (pause, reset, change speed, etc.)

๐Ÿ’ก Add mousePressed() or keyPressed() handlers to allow pausing/resuming, resetting the animation, or adjusting parameters like swing angle or cycle time in real-time.

STYLE initPendulums() - length mapping

The length mapping uses 'period * period' which is correct physics but not immediately obvious why it's squared.

๐Ÿ’ก Add a comment explaining that Tยฒ โˆ L (period squared is proportional to length) comes from real pendulum physics, making the code more educational.

Preview

Pendulum Wave Symphony - xelsed.ai - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Pendulum Wave Symphony - xelsed.ai - Code flow showing setup, initpendulums, draw, windowresized, constructor, update, display
Code Flow Diagram