Beachvibes

Beachvibes is an interactive audio-visual sketch that creates a relaxing beach scene with animated ocean waves synchronized to dynamic sound effects. The sketch combines continuous wave noise with periodic crashing wave sounds, triggered with visual splash animations, while users control volume, wave depth, and crash frequency through interactive sliders.

🧪 Try This!

Experiment with the code by making these changes:

  1. Change the sky color — The background color is set to light blue—change it to sunset orange, deep purple, or any RGB color you want
  2. Make the sun larger — The sun's third parameter is its diameter—increase it to create a bigger, more dramatic sun
  3. Make waves animate faster — The timeOffset increment controls wave animation speed—double it to see waves flow twice as fast
  4. Make crashes happen more frequently — Lower the default crash interval to trigger crashes every 2-3 seconds instead of waiting 6+ seconds
  5. Add a fourth wave layer — The waves are drawn in three layers—add a fourth with even deeper blue to increase visual depth
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a calming beach visualization complete with layered animated ocean waves, a sun, and dynamic water splash effects that synchronize with audio. It uses p5.sound library's noise generators, filter objects, and envelope control to produce both continuous wave-like ambient sounds and periodic crashing waves. The visual waves are drawn using Perlin noise and curveVertex for smooth, organically-moving shapes that loop seamlessly across the screen.

The code is organized into a setup() function that initializes all p5.sound objects and UI sliders, a draw() loop that updates the audio amplitude and frequency in real time while rendering animated wave layers and splash effects, and helper functions like drawWave() for the ocean visualization and windowResized() for responsive layout. By studying this sketch you will learn how to synthesize sound with p5.sound, modulate audio parameters with sliders, synchronize visuals to sound events, and create looping procedural animations using noise functions.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, initializes two separate white noise generators (one for continuous waves, one for crash sounds), applies lowpass and bandpass filters to shape their tones, and sets up four interactive sliders for volume, wave depth, crash interval, and crash volume control.
  2. Every frame, draw() updates the continuous wave sound by reading the volume slider and mapping its value through Perlin noise to create natural amplitude fluctuations, then updates the filter frequency based on the pitch slider to simulate deeper or shallower ocean sounds.
  3. The crash sound system uses a separate counter (crashTrigger) that increments each frame and triggers a crash envelope when it reaches the value set by the crash interval slider, creating periodic loud splashing sounds at user-controlled intervals.
  4. Three layers of ocean waves are drawn at different y-positions with increasing opacity, each using a noise-based calculation that loops both horizontally across the screen and vertically over time to create continuous rolling motion.
  5. When a crash sound is triggered, a white splash ellipse appears at a random x-position along the front wave, growing in size and fading in opacity over several frames to provide a visual impact that synchronizes with the audio event.
  6. The windowResized() function repositions all UI elements whenever the browser window changes size, keeping the sketch responsive and fully interactive.

🎓 Concepts You'll Learn

Audio synthesis with p5.soundNoise generators and filteringEnvelope control (ADSR)Perlin noise animationReal-time parameter modulationEvent-based visual triggersResponsive canvas layoutLayered visual composition

📝 Code Breakdown

setup()

setup() runs once when the sketch starts and is where you initialize all p5.sound objects BEFORE calling any methods on them. This prevents errors when trying to set ADSR values on envelopes that haven't been created yet. The structure here—creating noise generators, filters, and envelopes, then chaining them together with disconnect() and connect()—is the fundamental pattern for building custom audio in p5.sound.

function setup() {
  createCanvas(windowWidth, windowHeight);
  noiseDetail(8, 0.5); // Set noise detail for smoother visuals

  // --- Initialize all p5.sound related objects at the start of setup() ---
  // This helps prevent the 'setADSR' error by ensuring objects are defined
  // before any methods are called on them.
  noiseGen = new p5.Noise('white');
  waveFilter = new p5.Filter('lowpass');
  crashNoise = new p5.Noise('white');
  crashEnvelope = new p5.Envelope();
  crashFilter = new p5.Filter('bandpass');

  // --- Continuous Wave Audio Setup ---
  noiseGen.start();
  noiseGen.amp(0); // Start silent

  waveFilter.freq(200);
  waveFilter.res(1.5);

  noiseGen.disconnect();
  noiseGen.connect(waveFilter);
  waveFilter.connect();

  // --- Crashing Wave Audio Setup ---
  crashNoise.start();
  crashNoise.amp(0); // Start silent

  // Adjusted ADSR parameters for a 5-second crash
  crashEnvelope.setADSR(0.01, 1, 0.1, 4); // Attack, Decay, Sustain, Release
  crashEnvelope.setRange(1, 0); // Max amplitude, Min amplitude

  crashFilter.freq(2000); // Higher frequency for splash sound
  crashFilter.res(2); // Some resonance

  crashNoise.disconnect(); // Disconnect crashNoise from master
  crashNoise.connect(crashFilter); // Connect crashNoise to crashFilter
  crashFilter.connect(); // Connect crashFilter to master output

  // Important: User must interact with the page for audio to start in most browsers
  userStartAudio();

  // --- UI Elements ---
  // Volume slider for continuous wave
  volumeSlider = createSlider(0, 1, 0.5, 0.01);
  volumeSlider.position(10, 10);
  volumeSlider.style('width', '180px');
  volLabel = createDiv('Continuous Vol');
  volLabel.position(200, 10);
  volLabel.style('color', 'black');
  volLabel.style('font-size', '14px');

  // Filter frequency slider (Wave Depth)
  pitchSlider = createSlider(50, 1000, 200, 1);
  pitchSlider.position(10, 40);
  pitchSlider.style('width', '180px');
  pitchLabel = createDiv('Wave Depth / Filter Freq');
  pitchLabel.position(200, 40);
  pitchLabel.style('color', 'black');
  pitchLabel.style('font-size', '14px');

  // Crash Interval slider - default increased to avoid immediate overlap
  crashIntervalSlider = createSlider(60, 300, 360, 10); // Min frames, Max frames, Initial, Step
  crashIntervalSlider.position(10, 70);
  crashIntervalSlider.style('width', '180px');
  crashIntervalLabel = createDiv('Crash Interval (frames)');
  crashIntervalLabel.position(200, 70);
  crashIntervalLabel.style('color', 'black');
  crashIntervalLabel.style('font-size', '14px');

  // Crash Volume slider
  crashVolumeSlider = createSlider(0, 1, 0.7, 0.01);
  crashVolumeSlider.position(10, 100);
  crashVolumeSlider.style('width', '180px');
  crashVolumeLabel = createDiv('Crash Volume');
  crashVolumeLabel.position(200, 100);
  crashVolumeLabel.style('color', 'black');
  crashVolumeLabel.style('font-size', '14px');

  // Instructions for audio start
  audioInstructions = createDiv('Click or press a key to start audio');
  audioInstructions.position(10, 130);
  audioInstructions.style('color', 'black');
  audioInstructions.style('font-size', '14px');

  // Hide instructions after first user interaction (for audio)
  document.addEventListener('mousedown', hideAudioInstructions);
  document.addEventListener('keydown', hideAudioInstructions);
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

initialization Canvas and Noise Detail Configuration createCanvas(windowWidth, windowHeight); noiseDetail(8, 0.5);

Creates a full-window canvas and configures Perlin noise to produce smooth, organic wave patterns

initialization Continuous Wave Audio Chain noiseGen = new p5.Noise('white'); waveFilter = new p5.Filter('lowpass'); noiseGen.start(); noiseGen.amp(0); waveFilter.freq(200); waveFilter.res(1.5); noiseGen.disconnect(); noiseGen.connect(waveFilter); waveFilter.connect();

Creates and chains a white noise generator through a lowpass filter to produce the continuous wave sound effect

initialization Crash Wave Audio Chain with Envelope crashNoise = new p5.Noise('white'); crashEnvelope = new p5.Envelope(); crashFilter = new p5.Filter('bandpass'); crashNoise.start(); crashNoise.amp(0); crashEnvelope.setADSR(0.01, 1, 0.1, 4); crashEnvelope.setRange(1, 0); crashFilter.freq(2000); crashFilter.res(2); crashNoise.disconnect(); crashNoise.connect(crashFilter); crashFilter.connect();

Creates a separate noise generator, envelope controller, and bandpass filter for the crash sounds with a 5-second fade-out decay

initialization UI Slider Creation volumeSlider = createSlider(0, 1, 0.5, 0.01); volumeSlider.position(10, 10); pitchSlider = createSlider(50, 1000, 200, 1); pitchSlider.position(10, 40); crashIntervalSlider = createSlider(60, 300, 360, 10); crashIntervalSlider.position(10, 70); crashVolumeSlider = createSlider(0, 1, 0.7, 0.01); crashVolumeSlider.position(10, 100);

Creates four interactive sliders to control continuous volume, wave depth, crash interval, and crash volume, positioning them in the top-left corner

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, making the sketch responsive to window size
noiseDetail(8, 0.5);
Configures Perlin noise to use 8 octaves of detail with 0.5 falloff, creating smoother and more natural-looking waves
noiseGen = new p5.Noise('white');
Creates a white noise generator object (unfiltered noise containing all frequencies equally) for the continuous wave sound
waveFilter = new p5.Filter('lowpass');
Creates a lowpass filter that will remove high frequencies, making the noise sound deeper and more wave-like
noiseGen.start();
Starts the noise generator, making it ready to produce sound (though initially silenced with amp(0))
waveFilter.freq(200);
Sets the filter's cutoff frequency to 200 Hz, allowing only frequencies below 200 Hz to pass through, creating deep ocean tones
noiseGen.connect(waveFilter);
Routes the noise generator output through the filter instead of directly to the speaker, shaping its tone
crashEnvelope.setADSR(0.01, 1, 0.1, 4);
Configures the crash sound's attack (0.01s), decay (1s), sustain (0.1 amplitude), and release (4s) envelope—the sound fades out over 4 seconds
crashFilter.freq(2000);
Sets the crash filter's center frequency to 2000 Hz, emphasizing mid-range frequencies for a crisp splashing sound
userStartAudio();
Initializes the audio context and allows audio to start when the user clicks or presses a key (required by most browsers for security)

draw()

draw() runs 60 times per second and is where all animation and real-time updates happen. In this sketch, draw() reads slider values every frame to modulate the continuous sound, increments the crash timer, and triggers synchronized audio and visual events. The key insight is that even though audio and visuals seem connected, they're actually independent systems that you synchronize by using the same conditions or timers—when crashTrigger reaches the interval, BOTH the sound envelope plays AND the splash variables reset to start their animation.

🔬 This code triggers a crash every time crashTrigger reaches the interval value. What happens if you change crashTrigger = 0 to crashTrigger -= crashIntervalSlider.value() / 2 so the timer doesn't fully reset?

  // Trigger crash sound and visual effect periodically
  crashTrigger++;
  if (crashTrigger >= crashIntervalSlider.value()) {
    crashEnvelope.play(crashNoise); // Play the crash envelope
    crashTrigger = 0; // Reset timer

🔬 These three lines draw waves at different y-positions with different colors and speeds. What happens if you add a fourth drawWave call with different parameters, like drawWave(height * 0.7, 40, color(0, 50, 150, 160), 0.03)?

  // Draw multiple layers of waves for depth
  drawWave(height * 0.8, 60, color(0, 100, 200, 180), 0.05);
  drawWave(height * 0.9, 80, color(0, 150, 255, 220), 0.10);
  drawWave(height * 1.0, 100, color(0, 200, 255, 255), 0.15);
function draw() {
  // Update continuous wave audio based on slider values
  let overallVol = volumeSlider.value();
  let filterFreq = pitchSlider.value();
  let audioDynamicVol = noise(audioNoiseOffset) * overallVol * 0.5 + overallVol * 0.5;
  noiseGen.amp(audioDynamicVol, 0.1);
  waveFilter.freq(filterFreq, 0.1);
  audioNoiseOffset += 0.002;

  // --- Crashing Wave Audio Logic ---
  let crashVol = crashVolumeSlider.value();
  let crashFilterCenterFreq = map(pitchSlider.value(), 50, 1000, 1000, 4000); // Map pitch slider to higher range for crash filter
  crashFilter.freq(crashFilterCenterFreq, 0.01); // Update crash filter frequency
  crashNoise.amp(crashVol, 0.01); // Set base volume for crash noise

  // Trigger crash sound and visual effect periodically
  crashTrigger++;
  if (crashTrigger >= crashIntervalSlider.value()) {
    crashEnvelope.play(crashNoise); // Play the crash envelope
    crashTrigger = 0; // Reset timer

    // Reset visual splash effect
    splashX = random(width * 0.2, width * 0.8); // Random x-position for splash
    splashY = height * 0.9; // Splash at the front wave's base
    splashSize = 0;
    splashAlpha = 255;
  }

  // --- Visuals ---
  background(135, 206, 235); // Light blue for the sky

  // Draw a simple sun
  noStroke();
  fill(255, 200, 0);
  ellipse(width * 0.2, height * 0.2, 80);

  // Draw multiple layers of waves for depth
  drawWave(height * 0.8, 60, color(0, 100, 200, 180), 0.05);
  drawWave(height * 0.9, 80, color(0, 150, 255, 220), 0.10);
  drawWave(height * 1.0, 100, color(0, 200, 255, 255), 0.15);

  // Draw visual splash effect
  if (splashAlpha > 0) {
    fill(255, splashAlpha);
    noStroke();
    ellipse(splashX, splashY, splashSize, splashSize * 0.5); // Oval splash
    splashSize += SPLASH_GROW_SPEED;
    splashAlpha -= SPLASH_FADE_SPEED;
    if (splashSize > MAX_SPLASH_SIZE) splashSize = MAX_SPLASH_SIZE;
    if (splashAlpha < 0) splashAlpha = 0;
  }

  // Increment timeOffset for wave animation and loop it
  timeOffset += 0.005;
  if (timeOffset > TWO_PI) {
    timeOffset = 0;
  }
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

calculation Continuous Wave Volume Modulation let overallVol = volumeSlider.value(); let filterFreq = pitchSlider.value(); let audioDynamicVol = noise(audioNoiseOffset) * overallVol * 0.5 + overallVol * 0.5; noiseGen.amp(audioDynamicVol, 0.1); waveFilter.freq(filterFreq, 0.1); audioNoiseOffset += 0.002;

Reads slider values and uses Perlin noise to create natural-sounding amplitude fluctuations in the continuous wave sound while updating the filter frequency in real time

conditional Crash Sound and Splash Trigger crashTrigger++; if (crashTrigger >= crashIntervalSlider.value()) { crashEnvelope.play(crashNoise); crashTrigger = 0; splashX = random(width * 0.2, width * 0.8); splashY = height * 0.9; splashSize = 0; splashAlpha = 255; }

Increments a counter each frame and triggers the crash sound envelope plus a visual splash effect whenever the counter reaches the interval value from the slider

calculation Splash Visual Growth and Fade if (splashAlpha > 0) { fill(255, splashAlpha); noStroke(); ellipse(splashX, splashY, splashSize, splashSize * 0.5); splashSize += SPLASH_GROW_SPEED; splashAlpha -= SPLASH_FADE_SPEED; if (splashSize > MAX_SPLASH_SIZE) splashSize = MAX_SPLASH_SIZE; if (splashAlpha < 0) splashAlpha = 0; }

Animates the splash by growing its size and reducing its opacity each frame until it reaches maximum size or disappears

function-call Wave Rendering Layers drawWave(height * 0.8, 60, color(0, 100, 200, 180), 0.05); drawWave(height * 0.9, 80, color(0, 150, 255, 220), 0.10); drawWave(height * 1.0, 100, color(0, 200, 255, 255), 0.15);

Draws three overlapping wave layers at progressively lower positions with different amplitudes, speeds, and opacity values to create visual depth

calculation Time Offset Loop timeOffset += 0.005; if (timeOffset > TWO_PI) { timeOffset = 0; }

Increments the time offset that drives wave animation and resets it after one full cycle to create seamless looping

let overallVol = volumeSlider.value();
Reads the current position of the continuous volume slider (a number between 0 and 1)
let filterFreq = pitchSlider.value();
Reads the current position of the pitch slider (a number between 50 and 1000 representing filter frequency in Hz)
let audioDynamicVol = noise(audioNoiseOffset) * overallVol * 0.5 + overallVol * 0.5;
Uses Perlin noise to create a smoothly fluctuating volume value—it ranges from overallVol * 0.5 to overallVol * 1.5, making the sound pulse naturally like breathing waves
noiseGen.amp(audioDynamicVol, 0.1);
Sets the noise generator's amplitude to the calculated dynamic volume, with 0.1 second smoothing to prevent clicks and pops
waveFilter.freq(filterFreq, 0.1);
Updates the lowpass filter frequency based on the slider, smoothly transitioning over 0.1 seconds so the tone changes are musical
audioNoiseOffset += 0.002;
Increments the noise offset very slowly each frame—this moves through Perlin noise space to create continuous variation in amplitude
let crashVol = crashVolumeSlider.value();
Reads the crash volume slider to control how loud each crash sound will be
let crashFilterCenterFreq = map(pitchSlider.value(), 50, 1000, 1000, 4000);
Maps the pitch slider's value (50-1000) to a higher range (1000-4000) for the crash filter, so adjusting wave depth also affects crash tone
crashTrigger++;
Increments the crash counter by 1 every frame—when it reaches the interval value, a crash triggers
if (crashTrigger >= crashIntervalSlider.value()) {
Checks whether enough frames have passed (based on the slider) to trigger a new crash event
crashEnvelope.play(crashNoise);
Plays the crash envelope on the crash noise, triggering the ADSR shape that makes the sound swell and fade out
splashX = random(width * 0.2, width * 0.8);
Places the splash at a random x-position along the middle 60% of the screen width (avoiding the edges)
background(135, 206, 235);
Fills the entire canvas with light blue (RGB 135, 206, 235) each frame, erasing the previous frame and creating a continuous sky effect
ellipse(width * 0.2, height * 0.2, 80);
Draws a yellow sun circle at 20% from the left and 20% from the top, with a diameter of 80 pixels
splashSize += SPLASH_GROW_SPEED;
Makes the splash ellipse larger each frame by adding the SPLASH_GROW_SPEED constant (1.5 pixels per frame)
splashAlpha -= SPLASH_FADE_SPEED;
Reduces the splash's transparency each frame by subtracting SPLASH_FADE_SPEED (0.85 per frame), making it gradually disappear
timeOffset += 0.005;
Advances the wave animation time very slowly each frame—this creates the illusion of waves rolling across the screen

drawWave()

This function demonstrates a powerful technique: using Perlin noise in 2D or 3D space to create organic, animated shapes. By feeding cos(angle) and sin(angle) into the noise function, the wave wraps smoothly around a circle and loops seamlessly when drawn repeatedly. The timeOffset parameter that changes each frame in draw() makes the entire noise field animate smoothly, creating the illusion of waves flowing across the screen without any explicit sine-wave math.

🔬 This loop calculates the wave using circular trigonometry (cos and sin of angle). What happens if you simplify it to just use the x position directly, like noise(x / width + timeOffset * speed)? Does the wave still look organic?

  for (let x = 0; x <= width; x += wavePointsStep) {
    let angle = map(x, 0, width, 0, TWO_PI);
    let noiseVal = noise(cos(angle) + timeOffset * speed, sin(angle) + timeOffset * speed);
    let y = map(noiseVal, 0, 1, yOffset - amplitude, yOffset + amplitude);
    curveVertex(x, y);
  }

🔬 Notice the duplicate curveVertex lines at the start and end—these are control points that smooth the curve without being visually part of the wave. What happens if you remove one of the duplicates or change height to yOffset?

  beginShape();
  curveVertex(-width * 0.1, height);
  curveVertex(-width * 0.1, height);

  for (let x = 0; x <= width; x += wavePointsStep) {
    let angle = map(x, 0, width, 0, TWO_PI);
    let noiseVal = noise(cos(angle) + timeOffset * speed, sin(angle) + timeOffset * speed);
    let y = map(noiseVal, 0, 1, yOffset - amplitude, yOffset + amplitude);
    curveVertex(x, y);
  }

  curveVertex(width * 1.1, height);
  curveVertex(width * 1.1, height);
  endShape();
/**
 * Draws a single layer of ocean waves using p5.js noise and curveVertex.
 * The wave is designed to loop seamlessly across the screen and over time.
 * @param {number} yOffset - The base y-position for the wave (e.g., height * 0.8).
 * @param {number} amplitude - The maximum height of the wave crests and troughs.
 * @param {p5.Color} waveColor - The color to fill the wave shape.
 * @param {number} speed - A multiplier for the timeOffset to control wave animation speed.
 */
function drawWave(yOffset, amplitude, waveColor, speed) {
  fill(waveColor);
  noStroke();

  beginShape();
  curveVertex(-width * 0.1, height);
  curveVertex(-width * 0.1, height);

  for (let x = 0; x <= width; x += wavePointsStep) {
    let angle = map(x, 0, width, 0, TWO_PI);
    let noiseVal = noise(cos(angle) + timeOffset * speed, sin(angle) + timeOffset * speed);
    let y = map(noiseVal, 0, 1, yOffset - amplitude, yOffset + amplitude);
    curveVertex(x, y);
  }

  curveVertex(width * 1.1, height);
  curveVertex(width * 1.1, height);
  endShape();
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

for-loop Wave Point Calculation Loop for (let x = 0; x <= width; x += wavePointsStep) { let angle = map(x, 0, width, 0, TWO_PI); let noiseVal = noise(cos(angle) + timeOffset * speed, sin(angle) + timeOffset * speed); let y = map(noiseVal, 0, 1, yOffset - amplitude, yOffset + amplitude); curveVertex(x, y); }

Loops across the canvas width, calculating a noise-based y-position for each point to create an organic wavy shape that animates over time

function drawWave(yOffset, amplitude, waveColor, speed) {
Defines a function that takes four parameters: yOffset (vertical position), amplitude (height of waves), waveColor (fill color), and speed (animation speed multiplier)
fill(waveColor);
Sets the fill color for all shapes drawn from here on to the waveColor passed in, like blue or cyan
noStroke();
Disables outlines so the wave is filled with solid color and has no border
beginShape();
Starts recording a series of vertices to form a custom shape (the wave)
curveVertex(-width * 0.1, height);
Adds a control point 10% off the left edge at the bottom of the canvas (creates smooth curve anchoring)
for (let x = 0; x <= width; x += wavePointsStep) {
Loops from x=0 to the canvas width, stepping by wavePointsStep pixels each iteration (15 by default, so ~27 points across a typical screen)
let angle = map(x, 0, width, 0, TWO_PI);
Converts the x-position into an angle from 0 to 2π (360 degrees), so the wave wraps around a circle mathematically
let noiseVal = noise(cos(angle) + timeOffset * speed, sin(angle) + timeOffset * speed);
Uses 2D Perlin noise to generate a height value (0-1)—the two inputs are sine and cosine of the angle plus a time component that makes the noise change smoothly over time
let y = map(noiseVal, 0, 1, yOffset - amplitude, yOffset + amplitude);
Converts the noise value (0-1) into a y-position that oscillates between yOffset-amplitude (wave trough) and yOffset+amplitude (wave crest)
curveVertex(x, y);
Records a vertex at position (x, y) as part of the wave shape—curveVertex creates smooth curves between points
curveVertex(width * 1.1, height);
Adds a control point 10% off the right edge at the bottom to anchor the wave down and close the shape
endShape();
Finishes recording vertices and fills the entire shape with the waveColor, creating a solid wave layer

hideAudioInstructions()

This is a callback function that only runs when the user clicks or presses a key for the first time. Its job is to remove the instruction text and clean up the event listeners so the function won't run again. In p5.js, managing event listeners is important for performance and avoiding bugs—this pattern ensures the hide function executes exactly once.

function hideAudioInstructions() {
  if (audioInstructions) {
    audioInstructions.hide();
    document.removeEventListener('mousedown', hideAudioInstructions);
    document.removeEventListener('keydown', hideAudioInstructions);
  }
}
Line-by-line explanation (4 lines)
if (audioInstructions) {
Checks whether the audioInstructions element exists (is not null) before trying to hide it
audioInstructions.hide();
Hides the 'Click or press a key to start audio' text by removing it from display
document.removeEventListener('mousedown', hideAudioInstructions);
Removes the event listener that was calling this function, so it only runs once on the first interaction
document.removeEventListener('keydown', hideAudioInstructions);
Also removes the keyboard event listener to clean up and prevent the function from being called again

windowResized()

windowResized() is a special p5.js function that automatically runs whenever the browser window is resized. By repositioning all UI elements inside it, the sketch stays responsive and usable at any window size. This is why Beachvibes fills the entire screen and remains interactive even when resized.

/**
 * Adjusts canvas size and repositions UI elements when the window is resized.
 */
function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  volumeSlider.position(10, 10);
  volLabel.position(200, 10);
  pitchSlider.position(10, 40);
  pitchLabel.position(200, 40);
  crashIntervalSlider.position(10, 70);
  crashIntervalLabel.position(200, 70);
  crashVolumeSlider.position(10, 100);
  crashVolumeLabel.position(200, 100);
  if (audioInstructions) audioInstructions.position(10, 130);
}
Line-by-line explanation (4 lines)
resizeCanvas(windowWidth, windowHeight);
Automatically resizes the p5.js canvas to match the new window dimensions, redrawing everything at the new size
volumeSlider.position(10, 10);
Repositions the volume slider to maintain its position in the top-left corner (10 pixels from left, 10 pixels from top)
volLabel.position(200, 10);
Repositions its label to the right of the slider
if (audioInstructions) audioInstructions.position(10, 130);
Repositions the instruction text if it still exists (hasn't been hidden yet), checking first to avoid errors

📦 Key Variables

noiseGen p5.Noise

A white noise generator that produces the continuous wave sound

let noiseGen = new p5.Noise('white');
waveFilter p5.Filter

A lowpass filter that removes high frequencies from noiseGen, making it sound deep and wave-like

let waveFilter = new p5.Filter('lowpass');
crashNoise p5.Noise

A separate white noise generator specifically for the crashing wave sounds

let crashNoise = new p5.Noise('white');
crashEnvelope p5.Envelope

Controls the attack, decay, sustain, and release shape of the crash sound, making it swell and fade over time

let crashEnvelope = new p5.Envelope();
crashFilter p5.Filter

A bandpass filter that emphasizes specific frequencies in the crash sound for a crisp splashing tone

let crashFilter = new p5.Filter('bandpass');
volumeSlider p5.Renderer.HTML5 input

Interactive slider controlling the continuous wave sound volume from 0 to 1

let volumeSlider = createSlider(0, 1, 0.5, 0.01);
pitchSlider p5.Renderer.HTML5 input

Interactive slider controlling the wave depth (filter frequency) and crash filter frequency, from 50 to 1000 Hz

let pitchSlider = createSlider(50, 1000, 200, 1);
crashIntervalSlider p5.Renderer.HTML5 input

Interactive slider controlling how many frames pass between each crash sound, from 60 to 300 frames

let crashIntervalSlider = createSlider(60, 300, 360, 10);
crashVolumeSlider p5.Renderer.HTML5 input

Interactive slider controlling the loudness of crash sounds from 0 to 1

let crashVolumeSlider = createSlider(0, 1, 0.7, 0.01);
crashTrigger number

A counter that increments each frame and triggers a crash sound when it reaches the interval value

let crashTrigger = 0;
timeOffset number

A continuously incrementing value passed to the noise function to animate the waves over time

let timeOffset = 0;
wavePointsStep number

The pixel distance between wave vertex calculations—higher values create choppier but faster waves

let wavePointsStep = 15;
audioNoiseOffset number

A value passed to the noise function to create smooth amplitude fluctuations in the continuous wave sound

let audioNoiseOffset = 0;
splashX number

The x-coordinate where the visual splash appears when a crash sound is triggered

let splashX = -100;
splashY number

The y-coordinate (always near the bottom of the screen) where the splash appears

let splashY = -100;
splashSize number

The current diameter of the splash ellipse, which grows each frame until reaching MAX_SPLASH_SIZE

let splashSize = 0;
splashAlpha number

The transparency of the splash (0-255), which decreases each frame until the splash disappears

let splashAlpha = 0;
MAX_SPLASH_SIZE number

A constant defining the maximum diameter the splash can grow to before stopping

const MAX_SPLASH_SIZE = 150;
SPLASH_GROW_SPEED number

A constant defining how many pixels the splash grows per frame

const SPLASH_GROW_SPEED = 1.5;
SPLASH_FADE_SPEED number

A constant defining how much transparency is lost per frame, making the splash fade away

const SPLASH_FADE_SPEED = 0.85;

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG draw() - crash filter frequency mapping

The crash filter frequency is mapped from pitchSlider (50-1000) to (1000-4000), but this calculation happens every frame. If pitchSlider is at minimum (50), the crash filter will always be at 1000 Hz, which may not match the continuously adjusting wave filter frequency.

💡 Store the result of the map() call and use it for subsequent crash filter updates, or reconsider the frequency mapping range to better match the continuous wave filter's behavior.

PERFORMANCE drawWave() - vertex calculation loop

The loop uses curveVertex for every point across the entire canvas width. On large displays or with low wavePointsStep values, this can create hundreds of vertices per frame, which impacts rendering performance.

💡 Consider using regular vertex() instead of curveVertex() for interior points, only using curveVertex() at curve anchoring points, or increase the default wavePointsStep value to reduce vertex count.

STYLE setup() - UI positioning

All UI sliders are hard-coded to fixed pixel positions (10, 40, 70, 100). On very small mobile screens, these elements may overlap or extend beyond the visible area.

💡 Calculate slider positions relative to screen width or height, or use a responsive layout library to adapt the UI to different screen sizes more gracefully.

FEATURE draw() - splash effect

The splash effect is always white (RGB 255, 255, 255). It doesn't visually relate to the ocean colors and could be more visually integrated.

💡 Make the splash color a lighter version of the front wave color by using lerpColor() to blend wave color with white, or use the wave's cyan color for a more cohesive visual effect.

BUG setup() - event listener cleanup

The hideAudioInstructions function removes event listeners, but if the sketch is reloaded or setup() is called multiple times, duplicate listeners could accumulate.

💡 Before adding the event listeners in setup(), remove any existing ones with document.removeEventListener() to ensure only one listener pair is active.

🔄 Code Flow

Code flow showing setup, draw, drawwave, hideaudioinstructions, windowresized

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

graph TD start[Start] --> setup[setup] setup --> canvas-noise-setup[Canvas and Noise Detail Configuration] setup --> continuous-wave-audio[Continuous Wave Audio Chain] setup --> crash-wave-audio[Crash Wave Audio Chain with Envelope] setup --> slider-ui-setup[UI Slider Creation] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-noise-setup href "#sub-canvas-noise-setup" click continuous-wave-audio href "#sub-continuous-wave-audio" click crash-wave-audio href "#sub-crash-wave-audio" click slider-ui-setup href "#sub-slider-ui-setup" draw --> continuous-audio-update[Continuous Wave Volume Modulation] draw --> crash-trigger-logic[Crash Sound and Splash Trigger] draw --> splash-animation[Splash Visual Growth and Fade] draw --> wave-layers[Wave Rendering Layers] draw --> time-animation[Time Offset Loop] click draw href "#fn-draw" click continuous-audio-update href "#sub-continuous-audio-update" click crash-trigger-logic href "#sub-crash-trigger-logic" click splash-animation href "#sub-splash-animation" click wave-layers href "#sub-wave-layers" click time-animation href "#sub-time-animation" crash-trigger-logic -->|increments counter| crash-trigger[Counter Increment] crash-trigger -->|triggers envelope| crash-wave-audio crash-trigger -->|triggers splash effect| splash-animation splash-animation -->|grows size| splash-growth[Size Growth] splash-animation -->|reduces opacity| splash-opacity[Opacity Reduction] time-animation -->|increments time offset| time-offset[Time Offset Increment] time-animation -->|resets after cycle| time-reset[Time Reset] wave-layers --> wave-vertex-loop[Wave Point Calculation Loop] wave-vertex-loop -->|calculates y-position| wave-y-position[Noise-based Y-Position Calculation]

❓ Frequently Asked Questions

What visual effects does the Beachvibes sketch create?

The Beachvibes sketch generates dynamic wave animations and visual splash effects that simulate the ambiance of a beach environment.

How can users interact with the Beachvibes sketch?

Users can interact with the sketch by adjusting sliders for volume and pitch, influencing the audio experience and visual output.

What creative coding techniques are showcased in the Beachvibes sketch?

The sketch demonstrates sound synthesis, audio modulation, and real-time visual rendering, blending audio and graphics for an immersive experience.

Preview

Beachvibes - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Beachvibes - Code flow showing setup, draw, drawwave, hideaudioinstructions, windowresized
Code Flow Diagram