AI Wind Chimes - Peaceful Garden Sounds Click to make the chimes swing and play! 5 hanging tubes of

This sketch simulates a set of five hanging wind chime tubes that sway like pendulums and play a soft sine-wave tone whenever you click. Each tube has its own length, pitch, and swing speed, and a painted background of hills, sun, and grass gives the whole scene a peaceful garden feel.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the chimes ring longer — Raising damping closer to 1 means the pendulum loses energy much more slowly, so each chime keeps swinging for longer after a click.
  2. Add more chimes — Increasing NUM_CHIMES adds more tubes to the row, each with its own pitch and slightly longer length, creating a fuller sound.
  3. Recolor the tubes — Changing the stroke color used for the tube body instantly changes their appearance from silver to gold.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch draws five hanging chime tubes above a painted garden scene, and clicking anywhere sets them all swinging and ringing with soft sine-wave tones. Each tube is a small pendulum physics simulation - it has an angle, angular velocity, and a spring-like restoring force - so it swings and settles just like a real chime. The audio side uses p5.js's p5.Oscillator to generate a tone per tube, with longer tubes tuned to lower pitches using lerp(), and each strike fades the volume up quickly and back down slowly to mimic a real chime's decay.

The code is organized around a Chime class that bundles together the physics (update), the drawing (display), the geometry layout (updateGeometry), and the sound trigger (strike) for a single tube, while setup() and draw() manage the overall canvas and loop through the array of chimes. Studying this sketch teaches you how to model simple spring physics with three variables (angle, velocity, acceleration), how classes keep related state and behavior together, and how to trigger and shape sound with p5.Oscillator's amp() and freq() methods.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas and builds five Chime objects, each of which calculates its own x position, hanging length, and pitch based on its index
  2. Every frame, draw() paints the sky, hills, ground, and sun (drawBackground), draws the wooden bar (drawChimeFrame), then loops through every chime calling update() and display()
  3. update() runs simple pendulum physics: it computes a restoring force pulling the chime back toward angle zero (like a spring), adds that to the angle's velocity, applies damping so the swing slowly settles, and updates the angle
  4. display() uses translate() and rotate() to draw each tube's cord and body at its current swinging angle, then draws a small dot at the fixed pivot point
  5. When you click, mousePressed() starts the audio engine and calls strike() on every chime, which gives each tube a sudden angular velocity (so it visibly swings) and tells its oscillator to fade its volume up quickly and back down over 1.5 seconds
  6. If the browser window is resized, windowResized() resizes the canvas and calls updateGeometry() on every chime so the tubes reposition themselves without losing their current swing or sound

🎓 Concepts You'll Learn

Classes and objectsPendulum/spring physics simulationp5.Oscillator sound synthesistranslate/rotate transformslerp() for interpolationEvent-driven interaction (mousePressed)Responsive layout (windowResized)

📝 Code Breakdown

Chime constructor()

The constructor runs once per Chime object when it's created with 'new Chime(...)'. It's the perfect place to set up starting state and calculate anything that doesn't need to change every frame, like the tube's fixed frequency.

constructor(index, total) {
    this.index = index;
    this.total = total;

    // Pendulum physics state
    this.angle = 0;
    this.angleVel = 0;
    this.angleAcc = 0;
    this.damping = 0.995; // how quickly it slows down
    this.k = 0.02;        // springiness (restoring force)

    // Geometry (set in updateGeometry so it recomputes on resize)
    this.x0 = 0;
    this.y0 = 0;
    this.length = 0;
    this.updateGeometry();

    // Sound: longer tube = lower frequency
    const freqHigh = 880;  // short tube, higher pitch
    const freqLow = 220;   // long tube, lower pitch
    const t = total > 1 ? index / (total - 1) : 0;
    this.freq = lerp(freqHigh, freqLow, t);

    this.osc = new p5.Oscillator('sine');
    this.osc.freq(this.freq);
    this.osc.amp(0); // start silent
    this.osc.start();
  }
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Safe Division Guard const t = total > 1 ? index / (total - 1) : 0;

Avoids dividing by zero when there's only one chime, and normalizes the index to a 0-1 range for pitch interpolation

this.index = index;
Remembers which position (0 to 4) this chime occupies, used later for spacing and pitch
this.damping = 0.995; // how quickly it slows down
A multiplier applied to velocity each frame - values just under 1 slowly bleed off swing energy like air resistance
this.k = 0.02; // springiness (restoring force)
How strongly the chime is pulled back toward angle zero, like a spring constant
this.updateGeometry();
Calls the method that computes this chime's x/y position and tube length based on canvas size
const t = total > 1 ? index / (total - 1) : 0;
Converts the chime's index into a fraction from 0 to 1 so it can be used to interpolate pitch
this.freq = lerp(freqHigh, freqLow, t);
Blends between the high and low frequency based on t, so tube 0 is highest-pitched and the last tube is lowest-pitched
this.osc = new p5.Oscillator('sine');
Creates a sine-wave sound generator for this chime using the p5.sound library
this.osc.amp(0); // start silent
Sets the oscillator's volume to zero so it's silent until strike() is called
this.osc.start();
Starts the oscillator running continuously in the background - it's always 'on' but silent until amp is raised

updateGeometry()

This method is called both from the constructor and from windowResized(), so all size-dependent geometry lives in one place and automatically recalculates whenever the canvas changes size.

🔬 This caps how wide the chime row can spread using min(width * 0.6, 500). What happens on a wide screen if you raise 500 to 900, letting the chimes spread much further apart?

    const totalWidth = min(width * 0.6, 500);
    const spacing = this.total > 1 ? totalWidth / (this.total - 1) : 0;
    const startX = width / 2 - totalWidth / 2;
updateGeometry() {
    // Horizontal layout
    const totalWidth = min(width * 0.6, 500);
    const spacing = this.total > 1 ? totalWidth / (this.total - 1) : 0;
    const startX = width / 2 - totalWidth / 2;

    this.x0 = startX + this.index * spacing;
    this.y0 = height * 0.2;

    // Tube length increases slightly with index
    const baseLen = height * 0.25;
    const extra = height * 0.05 * this.index;
    this.length = baseLen + extra;
  }
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Safe Spacing Calculation const spacing = this.total > 1 ? totalWidth / (this.total - 1) : 0;

Prevents division by zero if there's only a single chime, otherwise spreads chimes evenly

const totalWidth = min(width * 0.6, 500);
Caps how wide the row of chimes can spread, so on huge screens they don't spread out too far apart
const spacing = this.total > 1 ? totalWidth / (this.total - 1) : 0;
Divides the total width evenly between all chimes to find the gap between each one's pivot point
const startX = width / 2 - totalWidth / 2;
Finds the left edge of the chime row so the whole group is centered on screen
this.x0 = startX + this.index * spacing;
Places this chime's pivot point using its index times the spacing, so chimes line up left to right
this.y0 = height * 0.2;
Fixes the pivot's vertical position near the top of the canvas, just under the wooden bar
const baseLen = height * 0.25;
Sets a starting tube length relative to canvas height so it scales with screen size
const extra = height * 0.05 * this.index;
Adds a bit more length for each successive chime, making later tubes longer (and lower-pitched)
this.length = baseLen + extra;
Combines the base length and the extra amount into this chime's final tube length

strike()

strike() is called once per click for every chime. It's the bridge between user interaction and both the visual physics (angleVel) and the audio (osc.amp), showing how a single event can kick off two different systems at once.

🔬 Right now the first chime (index 0) always swings the hardest. What happens if you flip the sign to -0.12 + 0.02 * this.index, making the LAST chime swing hardest instead?

    this.angleVel = -0.12 - 0.02 * this.index;
    this.angle = this.angleVel * 15;
strike() {
    // Set an initial swing (a bit different per tube)
    this.angleVel = -0.12 - 0.02 * this.index;
    this.angle = this.angleVel * 15;

    // Trigger sound with quick attack and gentle decay
    this.osc.freq(this.freq);
    this.osc.amp(0.4, 0.02); // fade up to 0.4 in 20ms
    this.osc.amp(0, 1.5);    // fade back to 0 over 1.5s
  }
Line-by-line explanation (5 lines)
this.angleVel = -0.12 - 0.02 * this.index;
Gives the chime a sudden angular velocity to start it swinging - later chimes (higher index) get a slightly stronger push
this.angle = this.angleVel * 15;
Immediately tilts the chime's angle so the swing is visible on the very first frame, instead of waiting for velocity to accumulate
this.osc.freq(this.freq);
Makes sure the oscillator is set to this chime's specific pitch before playing
this.osc.amp(0.4, 0.02); // fade up to 0.4 in 20ms
Ramps the volume up to 0.4 over just 20 milliseconds, creating a quick, percussive attack
this.osc.amp(0, 1.5); // fade back to 0 over 1.5s
Schedules the volume to fade back down to silence over 1.5 seconds, giving the tone a natural chime-like decay

update()

update() runs every single frame for every chime and implements the classic acceleration-velocity-position pattern used in almost all physics-based p5.js animations, just applied to an angle instead of x/y position.

🔬 This four-line loop is a tiny spring simulation. What happens if you remove the damping line (this.angleVel *= this.damping;) entirely - will the chime ever stop swinging?

    this.angleAcc = -this.k * this.angle;
    this.angleVel += this.angleAcc;
    this.angleVel *= this.damping;
    this.angle += this.angleVel;
update() {
    this.angleAcc = -this.k * this.angle;
    this.angleVel += this.angleAcc;
    this.angleVel *= this.damping;
    this.angle += this.angleVel;
  }
Line-by-line explanation (4 lines)
this.angleAcc = -this.k * this.angle;
Calculates a restoring force that always points back toward angle zero - the further the chime has swung, the stronger the pull back, just like a spring
this.angleVel += this.angleAcc;
Adds the acceleration to the velocity, speeding up or slowing down the swing
this.angleVel *= this.damping;
Multiplies velocity by a number slightly less than 1 every frame, gradually removing energy so the swing eventually stops
this.angle += this.angleVel;
Moves the angle forward by the current velocity, actually causing the rotation you see on screen

display()

display() shows the classic push()/translate()/rotate()/pop() pattern: by moving and rotating the coordinate system first, all the shapes can be drawn using simple local coordinates (like x=0) instead of complex trigonometry.

display() {
    // Tube + cord
    push();
    translate(this.x0, this.y0);
    rotate(this.angle);

    // Cord
    stroke(90, 80);
    strokeWeight(2);
    const cordLen = this.length * 0.15;
    line(0, 0, 0, cordLen);

    // Tube body
    const tubeTop = cordLen;
    const tubeBottom = this.length;
    strokeWeight(12);
    stroke(210);
    line(0, tubeTop, 0, tubeBottom);

    // Highlight stripe on the tube
    strokeWeight(5);
    stroke(240, 240, 255, 180);
    line(0, tubeTop + 2, 0, tubeBottom - 2);

    pop();

    // Small pivot dot
    noStroke();
    fill(80);
    circle(this.x0, this.y0, 6);
  }
Line-by-line explanation (10 lines)
push();
Saves the current drawing settings and transform so changes made here don't affect other chimes
translate(this.x0, this.y0);
Moves the origin (0,0) to this chime's pivot point, so all following drawing is relative to that point
rotate(this.angle);
Rotates the coordinate system by the chime's current swing angle, so everything drawn after this tilts with it
const cordLen = this.length * 0.15;
Calculates how long the top cord should be, as a fraction of the total tube length
line(0, 0, 0, cordLen);
Draws the thin cord from the pivot point downward to where the tube begins
strokeWeight(12);
Sets a thick line weight so the next line() call looks like a solid tube rather than a thin line
line(0, tubeTop, 0, tubeBottom);
Draws the main tube body as a thick vertical line from where the cord ends to the tube's full length
stroke(240, 240, 255, 180);
Sets a light, semi-transparent color for a highlight stripe, giving the tube a glossy, metallic look
pop();
Restores the saved transform and settings so the next chime (or other drawing) isn't affected by this rotation
circle(this.x0, this.y0, 6);
Draws a small dot at the fixed pivot point, drawn AFTER pop() so it stays still even while the tube swings

setup()

setup() runs exactly once when the sketch starts. It's the right place to build your canvas and populate arrays of objects like chimes before the animation loop begins.

function setup() {
  createCanvas(windowWidth, windowHeight);
  // Create chimes
  for (let i = 0; i < NUM_CHIMES; i++) {
    chimes.push(new Chime(i, NUM_CHIMES));
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Chime Creation Loop for (let i = 0; i < NUM_CHIMES; i++) {

Creates one Chime object per index from 0 to NUM_CHIMES-1 and adds it to the chimes array

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window
for (let i = 0; i < NUM_CHIMES; i++) {
Loops once for each chime, using i as that chime's index
chimes.push(new Chime(i, NUM_CHIMES));
Creates a new Chime object (running its constructor) and adds it to the end of the chimes array

draw()

draw() runs continuously about 60 times per second. Here it's kept very short by delegating work to helper functions and to each object's own update/display methods - a common and readable pattern for organizing p5.js sketches.

🔬 What happens if you swap the order and call chime.display() before chime.update()? Would the chime you see be one frame behind?

  for (let chime of chimes) {
    chime.update();
    chime.display();
  }
function draw() {
  drawBackground();
  drawChimeFrame();

  // Update and draw all chimes
  for (let chime of chimes) {
    chime.update();
    chime.display();
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Update and Draw All Chimes for (let chime of chimes) {

Loops through every Chime object, advancing its physics and drawing it in its new position each frame

drawBackground();
Repaints the entire sky, hills, ground, and sun every frame, which also clears the previous frame's drawing
drawChimeFrame();
Draws the wooden bar the chimes hang from, on top of the background
for (let chime of chimes) {
A for...of loop that goes through each Chime object in the chimes array one at a time
chime.update();
Advances this chime's swing physics by one frame
chime.display();
Draws this chime at its newly updated angle and position

drawBackground()

This function shows how a whole scenic background can be built from just a handful of primitive shapes (ellipse, rect, circle, triangle) drawn with proportional positions based on width and height, so it scales cleanly to any screen size.

🔬 This loop spaces grass tufts using width / 20. What happens if you change that to width / 60, packing in three times as many, smaller tufts?

  const step = width / 20;
  for (let x = 0; x < width; x += step) {
    const h = random(15, 30);
    triangle(x, height * 0.7, x + step * 0.3, height * 0.7 - h, x + step * 0.6, height * 0.7);
  }
function drawBackground() {
  // Soft sky
  background(206, 226, 255);

  noStroke();

  // Distant hills
  fill(180, 215, 190);
  ellipse(width * 0.2, height * 0.9, width * 0.8, height * 0.7);
  fill(170, 205, 185);
  ellipse(width * 0.8, height * 0.95, width * 0.9, height * 0.8);

  // Ground
  fill(190, 230, 200);
  rect(0, height * 0.7, width, height * 0.3);

  // Soft sun
  fill(255, 245, 210, 240);
  circle(width * 0.15, height * 0.2, min(width, height) * 0.18);

  // A few simple grass tufts for extra calmness
  fill(160, 210, 170, 180);
  const step = width / 20;
  for (let x = 0; x < width; x += step) {
    const h = random(15, 30);
    triangle(x, height * 0.7, x + step * 0.3, height * 0.7 - h, x + step * 0.6, height * 0.7);
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Grass Tuft Loop for (let x = 0; x < width; x += step) {

Draws a triangle-shaped tuft of grass at regular intervals across the width of the canvas

background(206, 226, 255);
Fills the whole canvas with a soft blue color, clearing the previous frame and forming the sky
ellipse(width * 0.2, height * 0.9, width * 0.8, height * 0.7);
Draws a large soft green ellipse to represent a distant hill, sized relative to canvas dimensions
rect(0, height * 0.7, width, height * 0.3);
Draws a rectangle covering the bottom 30% of the canvas to represent the ground
circle(width * 0.15, height * 0.2, min(width, height) * 0.18);
Draws a soft, semi-transparent circle in the upper-left area to represent a gentle sun
const step = width / 20;
Divides the canvas width into 20 equal segments to space out the grass tufts evenly
for (let x = 0; x < width; x += step) {
Loops across the canvas width, moving x forward by one 'step' each time
const h = random(15, 30);
Picks a random height for each grass tuft so they don't all look identical
triangle(x, height * 0.7, x + step * 0.3, height * 0.7 - h, x + step * 0.6, height * 0.7);
Draws a triangle shape rising up from the ground line, forming one tuft of grass

drawChimeFrame()

This function demonstrates rectMode() - a handy way to switch how rect() interprets its coordinates, and the importance of resetting drawing modes afterward so they don't leak into other parts of your sketch.

function drawChimeFrame() {
  // Wooden bar holding the chimes
  const barY = height * 0.2;
  const barWidth = min(width * 0.7, 600);
  const barHeight = 12;

  rectMode(CENTER);
  noStroke();
  fill(160, 120, 90);
  rect(width / 2, barY, barWidth, barHeight, 6);
  rectMode(CORNER);
}
Line-by-line explanation (5 lines)
const barY = height * 0.2;
Positions the bar at the same height as each chime's pivot point, so tubes appear to hang from it
const barWidth = min(width * 0.7, 600);
Caps how wide the wooden bar can get on very large screens
rectMode(CENTER);
Changes rect() so its x/y arguments describe the shape's center instead of its corner, making centering easier
rect(width / 2, barY, barWidth, barHeight, 6);
Draws the bar centered horizontally, with a small corner radius of 6 pixels for rounded edges
rectMode(CORNER);
Resets rectMode back to the default so other rect() calls elsewhere aren't affected

mousePressed()

mousePressed() is a p5.js event function that automatically runs whenever the mouse is clicked. It's the standard way to connect user interaction to changes in your sketch's state.

🔬 Right now every click strikes ALL five chimes at once. What happens if you strike just one random chime instead, using chimes[floor(random(chimes.length))].strike();?

  for (let chime of chimes) {
    chime.strike();
  }
function mousePressed() {
  // Make sure audio context is running (required on many browsers)
  userStartAudio();

  for (let chime of chimes) {
    chime.strike();
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Strike All Chimes Loop for (let chime of chimes) {

Calls strike() on every chime so all five tubes swing and play their tone together

userStartAudio();
Unlocks the browser's audio system - many browsers block sound until a user interaction like a click explicitly enables it
for (let chime of chimes) {
Loops through every chime in the array
chime.strike();
Triggers this chime's swing and sound, as defined in the strike() method

windowResized()

windowResized() is another automatic p5.js event function, called whenever the browser window changes size. Keeping oscillators alive here (instead of recreating chimes) avoids audio glitches during resize.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Recompute positions and lengths, keep oscillators
  for (let chime of chimes) {
    chime.updateGeometry();
  }
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

for-loop Recalculate Geometry Loop for (let chime of chimes) {

Calls updateGeometry() on every chime so their positions and lengths adapt to the new canvas size

resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the browser window's new dimensions
chime.updateGeometry();
Recalculates this chime's x/y position and tube length for the new canvas size, without recreating its oscillator or resetting its current swing

📦 Key Variables

chimes array

Holds all the Chime objects created in setup(); draw(), mousePressed(), and windowResized() all loop over this array to update, display, strike, or reposition every chime.

let chimes = [];
NUM_CHIMES number

A constant that sets how many chime tubes are created, used both in setup()'s loop and inside each Chime for spacing and pitch calculations.

const NUM_CHIMES = 5;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG drawBackground()

The grass tufts use random(15, 30) inside draw()'s background redraw, which runs every single frame - this means every grass tuft's height is randomized 60 times per second, causing visible flickering/jittering grass instead of a calm static scene.

💡 Generate the grass heights once in setup() and store them in an array, then use those stored values in drawBackground() instead of calling random() every frame.

FEATURE mousePressed()

Clicking anywhere on the canvas strikes all five chimes at once, so there's no way to play an individual tube or interact more precisely with the sound.

💡 Check the click position against each chime.x0 (using something like abs(mouseX - chime.x0) < threshold) and only call strike() on the nearest chime, letting users 'play' individual notes.

STYLE Chime.strike() and Chime.update()

Several 'magic numbers' appear inline (e.g., -0.12, 0.02, 15, 0.4, 1.5) without explanation of what they represent or why those specific values were chosen.

💡 Extract these into named constants or class fields (like this.strikeAmp = 0.4 and this.decayTime = 1.5) set in the constructor, making them easier to find and tune, and self-documenting.

PERFORMANCE Chime constructor()

Each Chime creates its own p5.Oscillator and starts it permanently, even though only 5 exist here; this pattern would not scale well if NUM_CHIMES were increased significantly, since every oscillator runs continuously in the audio thread.

💡 For larger numbers of chimes, consider using a shared pool of oscillators or triggering short synthesized envelopes on demand instead of keeping one oscillator per chime always running.

🔄 Code Flow

Code flow showing constructor, updategeometry, strike, update, display, setup, draw, drawbackground, drawchimeframe, mousepressed, windowresized

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> create-chimes-loop[create-chimes-loop] create-chimes-loop --> updategeometry[updategeometry] setup --> draw[draw loop] draw --> drawbackground[drawbackground] draw --> chime-loop[chime-loop] chime-loop --> update[update] update --> strike-all-loop[strike-all-loop] strike-all-loop --> strike[strike] strike --> freq-ternary[freq-ternary] chime-loop --> display[display] display --> grass-loop[grass-loop] draw --> mousepressed[mousepressed] draw --> windowresized[windowresized] windowresized --> resize-loop[resize-loop] resize-loop --> updategeometry click setup href "#fn-setup" click create-chimes-loop href "#sub-create-chimes-loop" click updategeometry href "#fn-updategeometry" click draw href "#fn-draw" click drawbackground href "#fn-drawbackground" click chime-loop href "#sub-chime-loop" click update href "#fn-update" click strike-all-loop href "#sub-strike-all-loop" click strike href "#fn-strike" click freq-ternary href "#sub-freq-ternary" click display href "#fn-display" click grass-loop href "#sub-grass-loop" click mousepressed href "#fn-mousepressed" click windowresized href "#fn-windowresized" click resize-loop href "#sub-resize-loop" click spacing-ternary href "#sub-spacing-ternary"

❓ Frequently Asked Questions

What visual elements does the AI Wind Chimes sketch display?

The sketch visually creates five hanging tubes of varying lengths that resemble wind chimes, swaying gently in response to user interactions.

How can users interact with the Wind Chimes simulation?

Users can click anywhere on the canvas to make the chimes swing and produce soothing musical tones.

What creative coding concept is demonstrated in this p5.js sketch?

This sketch showcases pendulum physics and sound synthesis, illustrating how physical simulations can be combined with audio generation for an interactive experience.

Preview

AI Wind Chimes - Peaceful Garden Sounds Click to make the chimes swing and play! 5 hanging tubes of - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Wind Chimes - Peaceful Garden Sounds Click to make the chimes swing and play! 5 hanging tubes of - Code flow showing constructor, updategeometry, strike, update, display, setup, draw, drawbackground, drawchimeframe, mousepressed, windowresized
Code Flow Diagram