AI Xylophone - Rainbow Musical Bars Click the colorful bars to play musical notes! A playful xyloph

This sketch renders an interactive xylophone with 8 rainbow-colored bars that play notes of a C major scale when clicked. Bars bounce with a spring animation and mallets follow the mouse cursor, all set against a soft gradient background inside a wooden frame.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the bounce bigger — Increasing the negative starting velocity makes bars leap higher when clicked, exaggerating the spring animation.
  2. Play louder notes — The first argument to amp() is the target volume - raising it makes every note noticeably louder.
  3. Grow the whole xylophone — Increasing maxHeight makes every bar taller, since minHeight and bar positions are all calculated relative to it.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a playable virtual xylophone: eight rainbow bars are laid out inside a wooden frame, and clicking one plays its musical note while the bar bounces like a real spring. It combines several p5.js techniques worth studying - the p5.Oscillator from the p5.sound library for audio synthesis, a simple spring-damper physics formula for the bounce, custom mouse-following mallet graphics, and an ES6 class (XyloBar) to keep each bar's data and behavior organized.

The code separates concerns cleanly: setup() and windowResized() handle canvas sizing and bar layout math, draw() just calls a handful of helper functions each frame, and the XyloBar class owns everything about a single bar - its position, its sound, its spring animation, and how it draws itself. Studying this sketch teaches you how to structure a small interactive instrument, how to fake physical bounce with math instead of a physics engine, and how to trigger sound safely in response to user interaction.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, builds 8 XyloBar objects (one per note in NOTE_DATA) each with its own color and oscillator, then calls layoutBars() to calculate where each bar should sit on screen.
  2. Every frame, draw() paints a soft gradient background, draws a wooden frame behind the bars, then loops through every bar calling update() (which advances its spring animation) and draw() (which renders it at its current bounce position), finally drawing two mallet shapes that follow the mouse cursor.
  3. When the user clicks, mousePressed() unlocks the audio context on the very first click (browsers require a user gesture before sound can play), then checks each bar's contains() method to see which bar was clicked.
  4. If a bar is clicked, its hit() method gives it an upward velocity kick and starts its oscillator, ramping the volume up quickly then fading it back to silence to create a percussive note.
  5. Every frame after a hit, the bar's update() method applies a spring formula (force pulls it back toward rest, damping slows it down) so the bar visibly bounces and settles rather than snapping back instantly.
  6. If the browser window is resized, windowResized() resizes the canvas and recalculates the whole layout so the xylophone always fits the screen.

🎓 Concepts You'll Learn

ES6 classesp5.sound oscillatorsspring/damper physicsmouse interaction and hit-testingresponsive layout with windowResizedlinear interpolation (lerp)custom cursor graphicsarray iteration with for...of

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It's the right place to configure the canvas and build any objects (like the bars array here) before the animation loop begins.

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke();
  textFont('sans-serif');
  colorMode(RGB);
  noCursor(); // hide mouse, we draw mallets instead

  // create bars
  for (let i = 0; i < NOTE_DATA.length; i++) {
    const note = NOTE_DATA[i];
    const col = BAR_COLORS[i % BAR_COLORS.length];
    bars.push(new XyloBar(i, note.name, note.freq, col));
  }

  layoutBars();
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

for-loop Create Bars for (let i = 0; i < NOTE_DATA.length; i++) {

Creates one XyloBar object for each note defined in NOTE_DATA, pairing it with a rainbow color

createCanvas(windowWidth, windowHeight);
Makes the canvas fill the entire browser window
noStroke();
Turns off outlines by default so shapes are drawn as solid fills unless stroke() is called later
textFont('sans-serif');
Sets the font used later when drawing note labels on the bars
colorMode(RGB);
Explicitly sets color mode to red/green/blue (the default), used for clarity
noCursor();
Hides the system mouse pointer since the sketch draws its own mallet graphics instead
for (let i = 0; i < NOTE_DATA.length; i++) {
Loops once for each of the 8 notes in the scale
const note = NOTE_DATA[i];
Grabs the note name and frequency for this position in the loop
const col = BAR_COLORS[i % BAR_COLORS.length];
Picks a rainbow color for this bar, wrapping around with modulo in case there are more notes than colors
bars.push(new XyloBar(i, note.name, note.freq, col));
Creates a new bar object and adds it to the bars array
layoutBars();
Calculates the size and position of every bar now that they all exist

draw()

draw() runs continuously (about 60 times per second). Keeping it short and delegating work to helper functions and object methods, like this sketch does, keeps the animation loop easy to read.

🔬 This loop updates and draws every bar in order. What happens visually if you swap the order to bar.draw() then bar.update() - does the bounce look one frame behind?

  for (let bar of bars) {
    bar.update();
    bar.draw();
  }
function draw() {
  // soft vertical gradient background
  drawBackgroundGradient();

  // wooden frame
  drawFrame();

  // update & draw each bar
  for (let bar of bars) {
    bar.update();
    bar.draw();
  }

  // mallets follow the mouse
  drawMallets();
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Update & Draw Bars for (let bar of bars) {

Advances each bar's spring animation and renders it at its current bounce position

drawBackgroundGradient();
Paints the soft blue gradient that fills the whole canvas behind everything else
drawFrame();
Draws the wooden frame rectangle that sits behind the bars
for (let bar of bars) {
Loops through every bar object stored in the bars array
bar.update();
Runs the spring-physics calculation that moves the bar back toward its resting position
bar.draw();
Renders the bar's rectangle, shadow, highlight, and note label at its current position
drawMallets();
Draws two mallet shapes that follow the current mouseX/mouseY position

mousePressed()

mousePressed() is a p5.js event function that automatically runs whenever the mouse button is clicked, making it the natural place to handle click-based interaction like triggering a note.

🔬 The break statement stops the loop after the first matching bar. What happens if you remove break - could overlapping bars both play?

  for (let bar of bars) {
    if (bar.contains(mouseX, mouseY)) {
      bar.hit(); // play note + bounce animation
      break;
    }
  }
function mousePressed() {
  // start audio context on first user interaction
  if (!audioStarted) {
    userStartAudio();
    audioStarted = true;
  }

  // check which bar was clicked
  for (let bar of bars) {
    if (bar.contains(mouseX, mouseY)) {
      bar.hit(); // play note + bounce animation
      break;
    }
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Unlock Audio Context if (!audioStarted) {

Starts the browser's audio engine on the very first click, which browsers require before any sound can play

for-loop Find Clicked Bar for (let bar of bars) {

Checks each bar to see if the mouse click landed inside it, and stops checking once one is found

if (!audioStarted) {
Only runs the audio-unlock code once, the very first time the user clicks anywhere
userStartAudio();
A p5.sound function that resumes the browser's audio context, required because browsers block autoplay sound until a user gesture occurs
audioStarted = true;
Remembers that audio has been unlocked so this block never runs again
for (let bar of bars) {
Loops through every bar to check which one, if any, was clicked
if (bar.contains(mouseX, mouseY)) {
Asks the bar itself whether the current mouse position is inside its rectangle
bar.hit(); // play note + bounce animation
Triggers the bar's note sound and starts its bounce animation
break;
Stops checking further bars once a match is found, since only one bar should respond per click

windowResized()

windowResized() is a built-in p5.js event that fires automatically whenever the browser window changes size, letting you keep responsive layouts.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  layoutBars();
}
Line-by-line explanation (2 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the new browser window dimensions
layoutBars();
Recalculates every bar's position and size so the layout still fits the new canvas size

layoutBars()

This function shows how to turn abstract layout rules ('bars get shorter left to right, bottoms aligned') into concrete pixel positions using lerp() and simple arithmetic - a pattern useful in any responsive p5.js layout.

🔬 This loop makes bars shrink from left to right using lerp(maxHeight, minHeight, t). What happens if you swap the arguments to lerp(minHeight, maxHeight, t) so bars grow taller toward the right instead?

  for (let i = 0; i < num; i++) {
    const t = i / (num - 1); // 0..1, left to right
    const h = lerp(maxHeight, minHeight, t); // left bars longer, right shorter
    const x = startX + i * (barWidth + spacing);
    const y = topY + (maxHeight - h); // align bottoms

    const bar = bars[i];
    bar.x = x;
    bar.y = y;
    bar.w = barWidth;
    bar.h = h;
  }
function layoutBars() {
  if (bars.length === 0) return;

  const num = bars.length;
  const totalWidth = width * 0.8;
  const spacing = totalWidth * 0.02;
  const barWidth = (totalWidth - spacing * (num - 1)) / num;

  const maxHeight = height * 0.35;
  const minHeight = height * 0.18;

  const startX = (width - totalWidth) / 2;
  const topY = (height - maxHeight) / 2;

  for (let i = 0; i < num; i++) {
    const t = i / (num - 1); // 0..1, left to right
    const h = lerp(maxHeight, minHeight, t); // left bars longer, right shorter
    const x = startX + i * (barWidth + spacing);
    const y = topY + (maxHeight - h); // align bottoms

    const bar = bars[i];
    bar.x = x;
    bar.y = y;
    bar.w = barWidth;
    bar.h = h;
  }

  // wooden frame around bars
  const padX = barWidth * 0.6;
  const padY = maxHeight * 0.3;
  frameRect.x = startX - padX;
  frameRect.y = topY - padY * 0.5;
  frameRect.w = totalWidth + padX * 2;
  frameRect.h = maxHeight + padY;
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Position Each Bar for (let i = 0; i < num; i++) {

Calculates the x, y, width and height for every bar so they line up left to right with decreasing height, bottoms aligned

if (bars.length === 0) return;
Safety check that exits early if there are no bars yet, avoiding math errors
const totalWidth = width * 0.8;
The whole row of bars will take up 80% of the canvas width
const barWidth = (totalWidth - spacing * (num - 1)) / num;
Divides the remaining space evenly among all bars after subtracting the gaps between them
const t = i / (num - 1); // 0..1, left to right
Converts the bar's index into a 0-to-1 fraction representing its position from left to right
const h = lerp(maxHeight, minHeight, t); // left bars longer, right shorter
Uses linear interpolation to smoothly shrink bar height from tallest (left) to shortest (right), mimicking a real xylophone
const y = topY + (maxHeight - h); // align bottoms
Shifts shorter bars downward so all bars share the same bottom edge, just like real xylophone bars
frameRect.w = totalWidth + padX * 2;
Makes the wooden frame slightly wider than the bars themselves so there's a visible border

drawFrame()

This function shows how push()/pop() let you safely change fill, stroke, and other style settings temporarily without affecting the rest of the sketch.

function drawFrame() {
  push();
  rectMode(CORNER);

  const r = min(width, height) * 0.03;
  const strokeW = min(width, height) * 0.015;

  // outer wooden frame
  stroke(90, 60, 30);
  strokeWeight(strokeW);
  fill(181, 129, 80);
  rect(frameRect.x, frameRect.y, frameRect.w, frameRect.h, r);

  // inner panel
  noStroke();
  fill(210, 160, 110);
  const inset = frameRect.w * 0.03;
  rect(
    frameRect.x + inset,
    frameRect.y + inset,
    frameRect.w - inset * 2,
    frameRect.h - inset * 2,
    r * 0.7
  );

  // little "screws" in the corners for fun
  const screwR = strokeW * 0.4;
  fill(140, 100, 60);
  const sx1 = frameRect.x + strokeW * 0.7;
  const sx2 = frameRect.x + frameRect.w - strokeW * 0.7;
  const sy1 = frameRect.y + strokeW * 0.7;
  const sy2 = frameRect.y + frameRect.h - strokeW * 0.7;
  circle(sx1, sy1, screwR * 2);
  circle(sx2, sy1, screwR * 2);
  circle(sx1, sy2, screwR * 2);
  circle(sx2, sy2, screwR * 2);

  pop();
}
Line-by-line explanation (7 lines)
push();
Saves the current drawing style settings so changes made in this function don't leak into other functions
const r = min(width, height) * 0.03;
Calculates a corner-rounding radius that scales with screen size
rect(frameRect.x, frameRect.y, frameRect.w, frameRect.h, r);
Draws the outer wooden frame rectangle with rounded corners using the frameRect values calculated in layoutBars()
const inset = frameRect.w * 0.03;
Calculates how far inward the lighter inner panel should sit from the outer frame edge
const screwR = strokeW * 0.4;
Sets the radius of the small decorative screw circles drawn in each corner
circle(sx1, sy1, screwR * 2);
Draws one of the four decorative screw circles at a calculated corner position
pop();
Restores the drawing style settings that were active before this function ran

drawBackgroundGradient()

lerpColor() blends smoothly between two colors, which combined with a per-row loop is a simple way to fake a gradient without needing WebGL shaders.

🔬 This loop draws one line per pixel for a smooth gradient. What happens to performance and appearance if you change 'y++' to 'y += 4' to only draw every 4th row?

  for (let y = 0; y < height; y++) {
    const t = y / max(1, height - 1);
    const c = lerpColor(topColor, bottomColor, t);
    stroke(c);
    line(0, y, width, y);
  }
function drawBackgroundGradient() {
  const topColor = color(240, 245, 255);
  const bottomColor = color(220, 235, 255);
  noFill();
  for (let y = 0; y < height; y++) {
    const t = y / max(1, height - 1);
    const c = lerpColor(topColor, bottomColor, t);
    stroke(c);
    line(0, y, width, y);
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Draw Gradient Row By Row for (let y = 0; y < height; y++) {

Draws one horizontal line per pixel row, blending from topColor to bottomColor to create a smooth vertical gradient

const topColor = color(240, 245, 255);
Defines the pale color used at the very top of the canvas
const bottomColor = color(220, 235, 255);
Defines the slightly darker blue color used at the very bottom of the canvas
for (let y = 0; y < height; y++) {
Loops once for every single pixel row on the canvas
const t = y / max(1, height - 1);
Converts the current row into a 0-to-1 fraction representing how far down the canvas it is
const c = lerpColor(topColor, bottomColor, t);
Blends the two colors together based on t, giving a color that gradually shifts from top to bottom color
line(0, y, width, y);
Draws a full-width horizontal line in that blended color, one row of the gradient

drawMallets()

This function demonstrates drawing composite shapes (multiple lines and circles) relative to the live mouseX/mouseY values every frame, a common technique for custom cursors.

function drawMallets() {
  push();
  const baseSize = min(width, height);
  const headR = baseSize * 0.025;
  const offsetX = headR * 2.2;
  const headY = mouseY - headR * 0.5;
  const handleLen = headR * 5.2;

  stroke(120, 80, 40);
  strokeWeight(headR * 0.4);
  strokeCap(ROUND);

  // left handle
  line(
    mouseX - offsetX,
    headY + headR * 0.2,
    mouseX - offsetX - handleLen * 0.3,
    headY + handleLen
  );
  // right handle
  line(
    mouseX + offsetX,
    headY + headR * 0.2,
    mouseX + offsetX + handleLen * 0.3,
    headY + handleLen
  );

  // mallet heads
  noStroke();
  fill(250);
  circle(mouseX - offsetX, headY, headR * 2);
  circle(mouseX + offsetX, headY, headR * 2);

  // inner shading
  fill(220);
  circle(mouseX - offsetX, headY, headR * 1.2);
  circle(mouseX + offsetX, headY, headR * 1.2);

  pop();
}
Line-by-line explanation (7 lines)
const headR = baseSize * 0.025;
Sets the mallet head radius as a fraction of the smaller screen dimension so it scales with window size
const offsetX = headR * 2.2;
How far left and right of the mouse each mallet head is placed
const headY = mouseY - headR * 0.5;
Positions the mallet heads slightly above the actual mouse Y coordinate
strokeCap(ROUND);
Makes the ends of the handle lines rounded instead of flat, for a softer look
line( mouseX - offsetX, headY + headR * 0.2, mouseX - offsetX - handleLen * 0.3, headY + handleLen );
Draws the left mallet's wooden handle from near the head down and outward
circle(mouseX - offsetX, headY, headR * 2);
Draws the round white mallet head on the left side of the cursor
fill(220);
Switches to a slightly darker gray for the inner shading circles that give the mallet heads a 3D look

XyloBar constructor()

The constructor runs once when 'new XyloBar(...)' is called, setting up all the initial state - layout placeholders, spring variables, and its own dedicated oscillator - that the rest of the class will use.

constructor(index, noteName, freq, col) {
    this.index = index;
    this.noteName = noteName;
    this.freq = freq;
    this.col = color(col);

    // layout values (set in layoutBars)
    this.x = 0;
    this.y = 0;
    this.w = 0;
    this.h = 0;

    // spring animation state
    this.yOffset = 0;
    this.vy = 0;

    // sound
    this.osc = new p5.Oscillator('sine');
    this.osc.freq(this.freq);
    this.osc.amp(0); // start silent
    this.oscStarted = false;
  }
Line-by-line explanation (7 lines)
this.col = color(col);
Converts the hex color string (like '#ff4b4b') into a p5.Color object that can be reused efficiently
this.yOffset = 0;
Stores how far the bar is currently displaced from its resting position due to the bounce animation
this.vy = 0;
Stores the bar's current vertical velocity for the spring physics
this.osc = new p5.Oscillator('sine');
Creates a sine-wave sound oscillator from the p5.sound library for this bar's note
this.osc.freq(this.freq);
Sets the oscillator's pitch to the bar's assigned frequency (e.g. 261.63 Hz for middle C)
this.osc.amp(0); // start silent
Sets the oscillator's volume to zero so it makes no sound until hit
this.oscStarted = false;
Tracks whether this oscillator has actually been started yet, since oscillators can only start after user interaction

XyloBar contains()

This is a classic axis-aligned bounding box (AABB) hit test - the same technique used for button clicks, collision detection, and drag-and-drop in countless p5.js sketches.

contains(px, py) {
    const yTop = this.y + this.yOffset;
    return (
      px >= this.x &&
      px <= this.x + this.w &&
      py >= yTop &&
      py <= yTop + this.h
    );
  }
Line-by-line explanation (3 lines)
const yTop = this.y + this.yOffset;
Uses the bar's current animated position (including any bounce offset) rather than its resting position, so clicks are accurate mid-bounce
px >= this.x && px <= this.x + this.w &&
Checks the point's x-coordinate falls within the bar's horizontal range
py >= yTop && py <= yTop + this.h
Checks the point's y-coordinate falls within the bar's current vertical range

XyloBar hit()

hit() is triggered from mousePressed() and shows a common ADSR-style envelope pattern (fast attack, slower release) built with just two calls to p5.Oscillator's amp() method.

🔬 The first line is the note's attack and the second its fade. What happens to the sound if you change the fade time from 0.4 to 2?

    this.osc.amp(0.5, 0.02); // target amplitude, ramp time
    this.osc.amp(0, 0.4);    // fade out
hit() {
    // bounce upwards
    this.vy = -4;

    // ensure oscillator is started (after userStartAudio)
    if (!this.oscStarted) {
      this.osc.start();
      this.osc.amp(0);
      this.oscStarted = true;
    }

    // quick attack, smooth release
    // https://p5js.org/reference/#/p5.Oscillator/amp
    this.osc.amp(0.5, 0.02); // target amplitude, ramp time
    this.osc.amp(0, 0.4);    // fade out
  }
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Start Oscillator Once if (!this.oscStarted) {

Starts the oscillator's audio node the first time this specific bar is hit, since starting it repeatedly is unnecessary and wasteful

this.vy = -4;
Gives the bar an instant upward velocity, kicking off the spring bounce animation
if (!this.oscStarted) {
Only runs the setup code the very first time this bar is hit
this.osc.start();
Actually begins generating sound from this oscillator (must happen after the audio context is unlocked)
this.osc.amp(0.5, 0.02); // target amplitude, ramp time
Quickly ramps the volume up to 0.5 over 0.02 seconds - a fast 'attack' that makes the note sound percussive
this.osc.amp(0, 0.4); // fade out
Immediately schedules the volume to ramp back down to silence over 0.4 seconds, creating the note's decay

XyloBar update()

This is a classic damped harmonic oscillator formula (F = -kx - cv) run once per frame, a lightweight alternative to a full physics engine for small bounce/wobble effects.

🔬 This is the whole spring simulation in 3 lines. What happens if you set damping to 0 - does the bar bounce forever instead of settling?

    const ay = -k * this.yOffset - damping * this.vy;
    this.vy += ay;
    this.yOffset += this.vy;
update() {
    // simple spring: a = -k * x - c * v
    const k = 0.15;     // spring stiffness
    const damping = 0.1; // damping coefficient

    const ay = -k * this.yOffset - damping * this.vy;
    this.vy += ay;
    this.yOffset += this.vy;

    // stop tiny jitter
    if (abs(this.vy) < 0.001 && abs(this.yOffset) < 0.001) {
      this.vy = 0;
      this.yOffset = 0;
    }
  }
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Stop Tiny Jitter if (abs(this.vy) < 0.001 && abs(this.yOffset) < 0.001) {

Snaps the bar exactly to rest once its motion becomes imperceptibly small, avoiding endless tiny floating-point wobble

const ay = -k * this.yOffset - damping * this.vy;
Calculates acceleration using Hooke's law: the further the bar is displaced (yOffset), the stronger the pull back toward zero, and damping resists the current velocity
this.vy += ay;
Updates velocity by adding this frame's acceleration - basic Euler integration
this.yOffset += this.vy;
Updates the bar's displacement by adding its velocity, actually moving it visually
if (abs(this.vy) < 0.001 && abs(this.yOffset) < 0.001) {
Checks if the bounce has become so small it's not worth animating anymore
this.vy = 0; this.yOffset = 0;
Forces the bar to rest exactly at zero offset instead of drifting forever with microscopic values

XyloBar draw()

This draw() method layers shapes - shadow, then main body, then highlight, then text - in back-to-front order, a common technique for giving flat 2D shapes a sense of depth.

draw() {
    push();
    rectMode(CORNER);
    const yTop = this.y + this.yOffset;

    // shadow
    noStroke();
    fill(0, 0, 0, 35);
    const shadowOffset = this.h * 0.08;
    rect(this.x, yTop + shadowOffset, this.w, this.h, this.w * 0.12);

    // main bar
    fill(this.col);
    rect(this.x, yTop, this.w, this.h, this.w * 0.12);

    // top highlight
    fill(255, 255, 255, 80);
    rect(
      this.x,
      yTop,
      this.w,
      this.h * 0.18,
      this.w * 0.12,
      this.w * 0.12,
      0,
      0
    );

    // note label in the center
    fill(255);
    textAlign(CENTER, CENTER);
    const ts = min(this.w * 0.3, this.h * 0.45);
    textSize(ts);
    text(this.noteName, this.x + this.w / 2, yTop + this.h / 2);

    pop();
  }
Line-by-line explanation (7 lines)
const yTop = this.y + this.yOffset;
Combines the bar's resting position with its current spring animation offset to get where it should actually be drawn this frame
rect(this.x, yTop + shadowOffset, this.w, this.h, this.w * 0.12);
Draws a semi-transparent dark rectangle slightly below the bar to fake a drop shadow
fill(this.col);
Switches to this bar's unique rainbow color before drawing its main body
rect(this.x, yTop, this.w, this.h, this.w * 0.12);
Draws the bar's main colored rectangle with rounded corners
fill(255, 255, 255, 80);
Switches to a semi-transparent white for the glossy highlight strip
const ts = min(this.w * 0.3, this.h * 0.45);
Calculates a text size that fits inside the bar regardless of its width or height, preventing overflow on very short or narrow bars
text(this.noteName, this.x + this.w / 2, yTop + this.h / 2);
Draws the note letter (like 'C') centered inside the bar

📦 Key Variables

bars array

Stores all 8 XyloBar objects that make up the instrument

let bars = [];
frameRect object

Stores the x, y, width and height of the wooden frame drawn behind the bars

let frameRect = { x: 0, y: 0, w: 0, h: 0 };
audioStarted boolean

Tracks whether the browser's audio context has been unlocked yet by a user click

let audioStarted = false;
NOTE_DATA array

A constant list of note names and frequencies (C major scale) used to build each bar

const NOTE_DATA = [{ name: 'C', freq: 261.63 }, ...];
BAR_COLORS array

A constant list of hex color strings assigned to bars in rainbow order

const BAR_COLORS = ['#ff4b4b', '#ff8c3a', ...];

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

PERFORMANCE drawBackgroundGradient()

The function draws one full-width line() for every single pixel row of the canvas, every single frame - on a 1080px tall window that's over a thousand draw calls per frame just for the background.

💡 Render the gradient once into an offscreen buffer with createGraphics() (or only regenerate it in windowResized()), then simply image() that buffer each frame instead of redrawing every row.

BUG layoutBars()

The line 'const t = i / (num - 1);' divides by zero if bars.length is ever 1, producing NaN and breaking the layout for every bar.

💡 Guard against num === 1 with something like 'const t = num > 1 ? i / (num - 1) : 0;'

FEATURE top-level (missing function)

The xylophone can currently only be played with a mouse or touch, which excludes keyboard-only users and makes it harder to play quick sequences of notes.

💡 Add a keyPressed() function that maps number keys 1-8 (or letters) to bars[i].hit(), so notes can be triggered from the keyboard too.

STYLE drawFrame() and layoutBars()

Many visual proportions (0.6, 0.3, 0.7, 0.03, etc.) are hardcoded as unnamed magic numbers scattered through the functions, making them hard to tune or understand at a glance.

💡 Pull the most important ratios into named constants near the top of the file (e.g. const FRAME_PAD_X_RATIO = 0.6;) so their purpose is clear and they're easy to find and adjust.

🔄 Code Flow

Code flow showing setup, draw, mousepressed, windowresized, layoutbars, drawframe, drawbackgroundgradient, drawmallets, xylobarconstructor, xylobarcontains, xylobarhit, xylobarupdate, xylobardraw

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> createbars[Create Bars] createbars --> create-bars-loop[Create Bars Loop] draw --> barsupdate[Update & Draw Bars] barsupdate --> bars-update-draw-loop[Bars Update & Draw Loop] draw --> drawbackgroundgradient[Draw Background Gradient] draw --> drawmallets[Draw Mallets] draw --> windowresized[windowResized] click setup href "#fn-setup" click draw href "#fn-draw" click createbars href "#fn-layoutbars" click create-bars-loop href "#sub-create-bars-loop" click barsupdate href "#fn-xylobarupdate" click bars-update-draw-loop href "#sub-bars-update-draw-loop" click drawbackgroundgradient href "#fn-drawbackgroundgradient" click drawmallets href "#fn-drawmallets" click windowresized href "#fn-windowresized" barsupdate --> unlockaudio[Unlock Audio] unlockaudio --> unlock-audio[Unlock Audio Context] click unlockaudio href "#sub-unlock-audio" barsupdate --> findclickedbar[Find Clicked Bar] findclickedbar --> find-clicked-bar[Find Clicked Bar Loop] click findclickedbar href "#sub-find-clicked-bar" barsupdate --> sizeandposition[Size and Position Bars] sizeandposition --> size-and-position-bars[Size and Position Bars Loop] click sizeandposition href "#sub-size-and-position-bars" drawbackgroundgradient --> gradientline[Gradient Line Loop] gradientline --> gradient-line-loop[Draw Gradient Row By Row] click gradientline href "#sub-gradient-line-loop" xylobarhit --> startoscillator[Start Oscillator Once] startoscillator --> start-oscillator-once[Start Oscillator Once] click startoscillator href "#sub-start-oscillator-once" xylobarhit --> settlejitter[Stop Tiny Jitter] settlejitter --> settle-jitter-check[Stop Tiny Jitter Check] click settlejitter href "#sub-settle-jitter-check"

❓ Frequently Asked Questions

What visual elements can I expect from the AI Xylophone sketch?

This sketch features eight colorful rectangular bars arranged horizontally, each representing a musical note, set against a soft gradient background.

How can I interact with the AI Xylophone sketch?

Users can click on the colorful bars to play corresponding musical notes, while mallets visually follow the mouse cursor for a playful experience.

What creative coding techniques are showcased in this p5.js sketch?

The sketch demonstrates object-oriented programming through the creation of XyloBar objects, as well as audio interaction using the p5.sound library.

Preview

AI Xylophone - Rainbow Musical Bars Click the colorful bars to play musical notes! A playful xyloph - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Xylophone - Rainbow Musical Bars Click the colorful bars to play musical notes! A playful xyloph - Code flow showing setup, draw, mousepressed, windowresized, layoutbars, drawframe, drawbackgroundgradient, drawmallets, xylobarconstructor, xylobarcontains, xylobarhit, xylobarupdate, xylobardraw
Code Flow Diagram