Neon Audio Visualizer - xelsed.ai

This sketch fakes a music visualizer using 32 vertical bars that rise and fall as if reacting to a beat, even though no real audio is involved. A purple-to-cyan color gradient sweeps across the bars, and a semi-transparent mirrored reflection fades out beneath them for a glossy, neon club aesthetic.

🧪 Try This!

Experiment with the code by making these changes:

  1. Change the bar count — Fewer bars look chunky and bold, while more bars create a denser, finer visualizer.
  2. Recolor the gradient — Swapping the starting color changes the entire look of the visualizer from purple-cyan to something completely different.
  3. Speed up the motion — Increasing the time multiplier makes the noise-driven wiggle animate much faster.
  4. Add motion trails — Lowering the background's opacity instead of fully clearing it each frame leaves faint glowing trails behind the moving bars.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates the illusion of an audio-reactive visualizer without ever touching a microphone or audio file. Thirty-two bars rise and fall using Perlin noise() combined with sin() oscillators, so the motion feels organic and musical rather than mechanically repetitive. A precomputed purple-to-cyan gradient colors each bar, and every bar is redrawn below the center line as a shrinking, semi-transparent reflection to mimic a glassy floor.

The code is short but layered: setup() runs once to build a reusable array of gradient colors with lerpColor(), while draw() runs 60 times per second to recompute each bar's height and brightness from a mix of noise, sine waves, and a global 'beat' value. A raw HTML5 canvas gradient (via drawingContext) is layered on top to fade the reflection into the background near the bottom of the screen. Studying this sketch teaches you how to fake convincing rhythmic motion with math alone, how to precompute expensive work in setup(), and how to mix p5.js drawing calls with lower-level canvas API calls.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, switches rectMode to CENTER so rectangles are positioned by their middle point, and precomputes 32 gradient colors ranging from purple to cyan using lerpColor().
  2. Every frame, draw() clears the screen with a near-black background and computes a slowly changing time value 't' from frameCount, which drives all the smooth motion.
  3. A single sine wave called globalBeat pulses up and down over time, acting like a shared 'heartbeat' that makes every bar swell and shrink together, simulating a rhythmic pulse.
  4. Inside a loop over all 32 bars, each bar's height comes from blending a noise() value (organic, unpredictable) with a local sin() oscillator (regular, wave-like) and then multiplying by globalBeat, before mapping that combined amplitude to a pixel height range.
  5. Each bar is drawn twice: once above the center line at full height and full color, and again below the center line at 70% height with reduced opacity to look like a reflection.
  6. Finally, a raw canvas gradient is painted over the bottom half of the screen to fade the reflection into darkness, and windowResized() keeps the canvas matching the browser window if it's resized.

🎓 Concepts You'll Learn

Perlin noise-based animationColor interpolation with lerpColorTrigonometric oscillation with sin()Mapping values with map() and constrain()Raw canvas gradients via drawingContextResponsive canvas resizing

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch starts, making it the ideal place for expensive one-time work like building a color palette. By precomputing barColors here instead of inside draw(), the sketch avoids calling lerpColor() 32 times every single frame - a good performance habit to learn early.

🔬 This loop builds the gradient array once at startup. What happens if you swap the arguments so it reads lerpColor(cyan, purple, t) instead - does the gradient direction flip?

  for (let i = 0; i < NUM_BARS; i++) {
    const t = i / (NUM_BARS - 1);
    barColors[i] = lerpColor(purple, cyan, t);
  }
function setup() {
  createCanvas(windowWidth, windowHeight);
  rectMode(CENTER);
  noStroke();
  colorMode(RGB);

  // Precompute a horizontal gradient from purple to cyan for the bars
  // https://p5js.org/reference/#/p5/lerpColor
  const purple = color(180, 50, 255);
  const cyan = color(0, 255, 255);

  for (let i = 0; i < NUM_BARS; i++) {
    const t = i / (NUM_BARS - 1);
    barColors[i] = lerpColor(purple, cyan, t);
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Precompute Gradient Colors for (let i = 0; i < NUM_BARS; i++) {

Calculates one interpolated color per bar, from purple to cyan, and stores them so draw() never has to recompute them every frame.

createCanvas(windowWidth, windowHeight)
Creates a canvas that fills the entire browser window.
rectMode(CENTER)
Changes rect() so its x/y arguments describe the shape's center instead of its top-left corner - important because bars are positioned by their middle point.
noStroke()
Turns off outlines on shapes so bars appear as solid, clean blocks of color.
colorMode(RGB)
Explicitly sets the color system to red/green/blue (this is actually p5's default, so this line is mostly for clarity).
const purple = color(180, 50, 255);
Defines the starting color of the gradient as a vivid purple.
const cyan = color(0, 255, 255);
Defines the ending color of the gradient as bright cyan.
const t = i / (NUM_BARS - 1);
Converts the bar index into a 0-to-1 fraction, so the first bar gets 0, the last bar gets 1, and everything in between is spread evenly.
barColors[i] = lerpColor(purple, cyan, t);
Blends purple and cyan by the fraction t and stores the result, so each bar gets its own step along the gradient.

draw()

draw() is the animation heartbeat of any p5.js sketch, running continuously (usually 60 times per second) to update and redraw the scene. This function is a great example of layering multiple math sources - noise(), sin(), and map() - to build motion that feels organic rather than robotic, and it also shows how you can escape p5.js's drawing API to use the underlying HTML5 Canvas 2D context directly via drawingContext for effects p5.js doesn't offer out of the box, like linear gradients.

🔬 This line blends 70% noise with 30% sine wave. What happens if you flip the weights to mostly sine (0.3 noise, 0.7 localOsc) - do the bars start moving in a more obviously rhythmic, wave-like pattern?

    // Combine noise, local oscillation, and global beat
    let amplitude = (noiseVal * 0.7 + localOsc * 0.3) * globalBeat; // up to ~1.4

🔬 This map() call caps bar height at 45% of the screen. What happens if you push the maximum up to 0.9 - do the top bars start crashing into the bottom reflections?

    const barHeight = map(
      amplitude,
      0, 1.4,
      height * 0.08,  // minimum height
      height * 0.45   // maximum height
    );
function draw() {
  // Dark background
  // https://p5js.org/reference/#/p5/background
  background(5, 5, 18);

  const centerY = height * 0.5;
  const sidePadding = width * 0.08;
  const usableWidth = width - sidePadding * 2;
  const step = usableWidth / NUM_BARS;
  const barWidth = step * 0.6;

  // Time parameters for smooth motion
  const t = frameCount * 0.02;

  // Global "beat" that makes everything pulse together
  // https://p5js.org/reference/#/p5/sin
  const beatPhase = frameCount * 0.15; // controls pulse speed
  let globalBeat = map(sin(beatPhase), -1, 1, 0.5, 1.4); // 0.5–1.4
  // https://p5js.org/reference/#/p5/map

  for (let i = 0; i < NUM_BARS; i++) {
    const xCenter = sidePadding + step * i + step * 0.5;

    // Smooth per-bar variation using noise and a local sine oscillator
    // https://p5js.org/reference/#/p5/noise
    const noiseVal = noise(i * 0.3, t);                 // 0..1
    const localOsc = (sin(t + i * 0.4) + 1) * 0.5;      // 0..1

    // Combine noise, local oscillation, and global beat
    let amplitude = (noiseVal * 0.7 + localOsc * 0.3) * globalBeat; // up to ~1.4

    // Map amplitude to bar height
    const barHeight = map(
      amplitude,
      0, 1.4,
      height * 0.08,  // minimum height
      height * 0.45   // maximum height
    );

    // Pulse brightness with the beat
    const baseColor = barColors[i];
    let pulseStrength = map(globalBeat, 0.5, 1.4, 0.7, 1.2);
    pulseStrength = constrain(pulseStrength, 0.7, 1.2);

    let r = red(baseColor) * pulseStrength;
    let g = green(baseColor) * pulseStrength;
    let b = blue(baseColor) * pulseStrength;
    r = constrain(r, 0, 255);
    g = constrain(g, 0, 255);
    b = constrain(b, 0, 255);

    // Top bar (above the center line)
    const topHeight = barHeight;
    const topCenterY = centerY - topHeight * 0.5;

    fill(r, g, b);
    rect(xCenter, topCenterY, barWidth, topHeight, barWidth / 2);

    // Reflection (below the center line)
    const reflectionHeight = barHeight * 0.7;
    const reflectionCenterY = centerY + reflectionHeight * 0.5;

    fill(r, g, b, 90); // semi-transparent reflection
    rect(xCenter, reflectionCenterY, barWidth, reflectionHeight, barWidth / 2);
  }

  // Fade out the reflection towards the bottom with a gradient mask
  const ctx = drawingContext; // https://p5js.org/reference/#/p5/drawingContext
  const gradient = ctx.createLinearGradient(0, centerY, 0, height);
  gradient.addColorStop(0, 'rgba(0,0,0,0)');
  gradient.addColorStop(1, 'rgba(0,0,0,0.9)');
  ctx.fillStyle = gradient;
  ctx.fillRect(0, centerY, width, height - centerY);
}
Line-by-line explanation (25 lines)

🔧 Subcomponents:

for-loop Draw Each Bar and Its Reflection for (let i = 0; i < NUM_BARS; i++) {

Loops through all 32 bars, computing each one's position, animated height, pulsing color, and drawing both the bar and its faded reflection below the center line.

calculation Global Beat Pulse let globalBeat = map(sin(beatPhase), -1, 1, 0.5, 1.4); // 0.5–1.4

Creates one shared oscillating value that every bar multiplies into its height and brightness, making all bars pulse together like they're reacting to a beat.

calculation Bottom Fade Gradient const gradient = ctx.createLinearGradient(0, centerY, 0, height);

Uses the raw HTML5 canvas API to paint a black gradient over the reflection area so it fades to darkness near the bottom of the screen.

background(5, 5, 18);
Repaints the entire canvas with a near-black navy color every frame, erasing the previous frame's bars so the animation doesn't smear.
const centerY = height * 0.5;
Finds the vertical middle of the canvas - this is the horizon line that bars grow up from and reflections hang below.
const sidePadding = width * 0.08;
Reserves 8% of the width as empty margin on the left and right so bars don't touch the screen edges.
const step = usableWidth / NUM_BARS;
Divides the remaining width evenly among all bars, giving each one its own horizontal slot.
const barWidth = step * 0.6;
Makes each bar only 60% as wide as its slot, leaving visible gaps between neighboring bars.
const t = frameCount * 0.02;
Turns the ever-increasing frameCount into a slow-moving time value used to animate the noise pattern smoothly.
const beatPhase = frameCount * 0.15; // controls pulse speed
Creates a faster-moving time value specifically for the shared pulse, independent from the per-bar motion speed.
let globalBeat = map(sin(beatPhase), -1, 1, 0.5, 1.4); // 0.5–1.4
Takes a sine wave (which naturally oscillates between -1 and 1) and remaps it to a 0.5-to-1.4 range, so it can be used as a multiplier that shrinks or boosts values.
const xCenter = sidePadding + step * i + step * 0.5;
Calculates the horizontal center position of bar number i by adding the padding, the space for previous bars, and half a slot to center it.
const noiseVal = noise(i * 0.3, t); // 0..1
Samples Perlin noise using the bar's index and the current time, producing a smoothly changing but unpredictable value between 0 and 1 unique to each bar.
const localOsc = (sin(t + i * 0.4) + 1) * 0.5; // 0..1
Uses a sine wave offset by the bar's index so neighboring bars are slightly out of phase, then rescales the result from -1..1 into 0..1.
let amplitude = (noiseVal * 0.7 + localOsc * 0.3) * globalBeat; // up to ~1.4
Blends the noise and the sine oscillator (weighted 70/30) and multiplies by the global beat, combining organic randomness, rhythmic wobble, and shared pulsing into one number.
const barHeight = map( amplitude, 0, 1.4, height * 0.08, // minimum height height * 0.45 // maximum height );
Converts the abstract amplitude number into an actual pixel height, guaranteeing bars are never shorter than 8% of the screen or taller than 45%.
let pulseStrength = map(globalBeat, 0.5, 1.4, 0.7, 1.2);
Reuses the global beat value to also control brightness, mapping it into a 0.7-to-1.2 multiplier.
pulseStrength = constrain(pulseStrength, 0.7, 1.2);
Clamps the multiplier so rounding or edge cases never push brightness outside the intended range.
let r = red(baseColor) * pulseStrength;
Extracts the red channel from the bar's precomputed gradient color and scales it by the pulse strength to make it brighter or dimmer with the beat.
r = constrain(r, 0, 255);
Keeps the red value inside the valid 0-255 color range after multiplying, preventing color glitches.
fill(r, g, b);
Sets the fill color for the upcoming shape using the pulsing red, green, and blue values.
rect(xCenter, topCenterY, barWidth, topHeight, barWidth / 2);
Draws the main bar above the center line; the fifth argument rounds the corners by half the bar's width, giving it a pill shape.
const reflectionHeight = barHeight * 0.7;
Makes the reflection shorter than the real bar (70% of its height) so it looks like a fading mirror image rather than an exact duplicate.
fill(r, g, b, 90); // semi-transparent reflection
Sets a translucent version of the same color (alpha 90 out of 255) so the reflection looks like it's glowing faintly rather than solid.
const gradient = ctx.createLinearGradient(0, centerY, 0, height);
Drops down to the raw HTML5 Canvas API to create a gradient object that transitions vertically from the center line to the bottom of the screen.
gradient.addColorStop(0, 'rgba(0,0,0,0)');
Makes the top of the gradient fully transparent black, so it has no effect right at the center line.
gradient.addColorStop(1, 'rgba(0,0,0,0.9)');
Makes the bottom of the gradient almost fully opaque black, fading the reflection out into the background.
ctx.fillRect(0, centerY, width, height - centerY);
Paints the gradient over the entire bottom half of the canvas, layering it on top of the already-drawn reflections.

windowResized()

windowResized() is a special p5.js event function that p5 automatically calls whenever the browser window changes size. Pairing it with resizeCanvas() is the standard way to build sketches that stay full-screen and responsive across different devices and window sizes.

function windowResized() {
  // https://p5js.org/reference/#/p5/resizeCanvas
  resizeCanvas(windowWidth, windowHeight);
}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the browser window's current width and height, so the visualizer always fills the screen even after resizing.

📦 Key Variables

NUM_BARS number

The total number of vertical bars drawn across the screen; used to size the loop and space out each bar's position.

const NUM_BARS = 32;
barColors array

Stores the precomputed purple-to-cyan gradient color for each bar so draw() doesn't have to recalculate colors every frame.

let barColors = [];

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

PERFORMANCE draw()

A new canvas gradient object is created every single frame via ctx.createLinearGradient(), even though centerY and height only change when the window is resized.

💡 Build the gradient once in setup() and windowResized(), store it in a variable, and simply reuse it inside draw() to avoid unnecessary object creation 60 times per second.

STYLE draw()

Magic numbers like 0.7, 0.3, 1.4, 0.6, and 0.08 are scattered throughout the function without explanation of what they represent.

💡 Pull these into named constants at the top of the sketch (e.g. NOISE_WEIGHT, OSC_WEIGHT, MIN_HEIGHT_RATIO) so their purpose is clear and they're easier to tune.

FEATURE draw()

Despite being called an 'audio visualizer,' the sketch never listens to real sound - all motion is simulated with noise() and sin().

💡 Use p5.sound's p5.AudioIn and p5.FFT to analyze microphone or file input and drive barHeight from actual frequency data for a genuinely audio-reactive visualizer.

STYLE setup()

colorMode(RGB) is called even though RGB is already p5.js's default color mode, adding a line that does nothing.

💡 Remove the redundant call, or replace it with colorMode(RGB, 255, 255, 255, 255) if you want to be explicit about the value ranges being used.

🔄 Code Flow

Code flow showing setup, draw, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> gradientloop[gradient-loop] draw --> barloop[bar-loop] draw --> globalbeatcalc[global-beat-calc] draw --> gradientmask[gradient-mask] gradientloop -->|Precompute Colors| draw barloop -->|Draw Each Bar| draw globalbeatcalc -->|Calculate Shared Pulse| barloop gradientmask -->|Apply Gradient| draw click setup href "#fn-setup" click draw href "#fn-draw" click gradientloop href "#sub-gradient-loop" click barloop href "#sub-bar-loop" click globalbeatcalc href "#sub-global-beat-calc" click gradientmask href "#sub-gradient-mask"

❓ Frequently Asked Questions

What visual effects does the Neon Audio Visualizer sketch create?

The Neon Audio Visualizer features 32 bouncing bars that pulse rhythmically with a stunning purple-to-cyan gradient and a mirror reflection effect, creating a mesmerizing music-reactive display.

Is the Neon Audio Visualizer interactive for users?

The sketch is not interactive; it automatically generates visuals based on simulated audio reactions without user input.

What creative coding techniques are showcased in the Neon Audio Visualizer?

This sketch demonstrates the use of noise functions and sine oscillators to create smooth, rhythmic motion in a visually dynamic audio visualizer.

Preview

Neon Audio Visualizer - xelsed.ai - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Neon Audio Visualizer - xelsed.ai - Code flow showing setup, draw, windowresized
Code Flow Diagram