Sketch 2025-11-27 02:28

This sketch animates a circle moving along an elliptical path while a sine wave oscillator's pitch follows the circle's vertical position. Click to start the sound, and watch as the circle's height directly controls the frequency of the tone you hear.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the orbit — Multiply frameCount by 0.2 instead of 0.05 to make the circle orbit four times faster; you will hear the pitch glide up and down more rapidly.
  2. Flip the pitch range — Swap 800 and 200 in the frequency map so the bottom of the screen plays high pitches and the top plays low pitches—the opposite of the original.
  3. Make the ellipse wider — Change (width / 4) to (width / 2.5) in the x calculation to stretch the ellipse horizontally; the circle will travel farther left and right.
  4. Use a square wave instead of sine — Change "sine" to "square" to create a hollow, synthesizer-like tone instead of the smooth sine wave.
  5. Increase pitch range — Change 800 and 200 to 1200 and 100 respectively to create a much wider pitch range that stretches from very high to very low.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a mesmerizing audio-visual experience by pairing animation with real-time sound synthesis. A bright pink circle orbits an elliptical path on the canvas, and as it moves, a Tone.js sine wave oscillator tracks its vertical position, translating height into pitch. When the circle is near the top of the screen, the pitch is high; as it descends, the tone drops lower. It combines three powerful p5.js and Web Audio techniques: the animation loop for smooth motion, trigonometric functions to calculate positions on an ellipse, and the map() function to link visual data to audio parameters.

The code is organized into setup() to initialize the canvas, draw() to animate the circle and update the oscillator frequency, mousePressed() to start the audio context on user interaction, and windowResized() to handle responsive scaling. By studying it, you will learn how to bridge visual and audio domains, use trigonometric functions for curved motion, and safely initialize Web Audio with Tone.js in response to user input.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas and centers all text alignment for instructions
  2. Every frame, draw() clears the background and calculates a new angle based on frameCount, which increases by 0.05 each frame
  3. Using that angle, the sketch calculates the circle's position on an ellipse: x and y are computed using cos(angle) and sin(angle) multiplied by the canvas dimensions, centering the orbit on the middle of the screen
  4. The circle is drawn at this new position with a 50-pixel diameter in bright pink
  5. If audio has started, the circle's y position is mapped to a frequency between 800 Hz (top of screen) and 200 Hz (bottom), and the oscillator smoothly transitions to that frequency over 50 milliseconds
  6. Before audio starts, instruction text appears on screen; clicking anywhere triggers mousePressed(), which initializes Tone.js, creates a sine wave oscillator at 440 Hz, and sets audioStarted to true
  7. The windowResized() function ensures the canvas stretches to fill the full window if the user resizes their browser

🎓 Concepts You'll Learn

Animation loopTrigonometric functions (cos, sin)Elliptical motionAudio synthesisTone.js oscillatorData mapping (map function)User interaction (mousePressed)Web Audio APIResponsive canvas

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch starts. It is the ideal place to initialize your canvas, set up variables, and configure p5.js settings that should stay constant throughout the sketch's lifetime.

function setup() {
  createCanvas(windowWidth, windowHeight);
  textAlign(CENTER, CENTER);
  textSize(16);
}
Line-by-line explanation (3 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, using built-in p5.js variables that give the window's current width and height in pixels
textAlign(CENTER, CENTER);
Tells p5.js that all text drawn after this line should be centered horizontally and vertically, making it easier to position text at exact coordinates
textSize(16);
Sets the font size to 16 pixels for all text drawn in the sketch

draw()

draw() is called 60 times per second by default, creating the illusion of smooth motion. Every frame, we recalculate the circle's position using trigonometric functions, update the oscillator's frequency, and redraw everything. This is the heartbeat of the animation.

🔬 These two lines create an ellipse by combining a center position (width / 2, height / 2) with trigonometric offsets. What happens if you change (width / 4) to (width / 3) in the x line only? How does the ellipse's shape change?

  // Elliptical path for the circle
  let x = width / 2 + (width / 4) * cos(angle);
  let y = height / 2 + (height / 4) * sin(angle);

🔬 This block maps the circle's vertical position to pitch. What happens if you change 0.05 to 0.2 or 0.01? How does the speed of pitch change affect what you hear?

    // Map y (top to bottom) to frequency (high to low)
    // Top of the screen -> higher pitch, bottom -> lower pitch
    let freq = map(y, 0, height, 800, 200); // Hz
    osc.frequency.rampTo(freq, 0.05);       // smooth change over 50 ms
function draw() {
  background(220);
  fill(255, 0, 100);

  // Angle controlling motion along the ellipse
  let angle = frameCount * 0.05;

  // Elliptical path for the circle
  let x = width / 2 + (width / 4) * cos(angle);
  let y = height / 2 + (height / 4) * sin(angle);

  // Draw the moving circle
  circle(x, y, 50);

  // If audio is running, map the y-position to oscillator frequency
  if (audioStarted && osc) {
    // Map y (top to bottom) to frequency (high to low)
    // Top of the screen -> higher pitch, bottom -> lower pitch
    let freq = map(y, 0, height, 800, 200); // Hz
    osc.frequency.rampTo(freq, 0.05);       // smooth change over 50 ms
  }

  // On-screen instruction (only before audio starts)
  if (!audioStarted) {
    fill(0);
    text(
      "Click anywhere to start the oscillator.\nPitch follows the circle's height.",
      width / 2,
      height * 0.1
    );
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation Elliptical Position Calculation let x = width / 2 + (width / 4) * cos(angle); let y = height / 2 + (height / 4) * sin(angle);

Uses trigonometric functions (cos and sin) to calculate x and y positions on an elliptical orbit based on the current angle

conditional Audio Frequency Update if (audioStarted && osc) { let freq = map(y, 0, height, 800, 200); osc.frequency.rampTo(freq, 0.05); }

Converts the circle's y position to an oscillator frequency and smoothly transitions the pitch over 50 milliseconds

conditional Instruction Text Display if (!audioStarted) { fill(0); text( "Click anywhere to start the oscillator.\nPitch follows the circle's height.", width / 2, height * 0.1 ); }

Shows instruction text on screen only before the user has clicked to start audio

background(220);
Clears the entire canvas with a light gray color (220 on a 0-255 brightness scale) every frame, preventing trails of past drawings
fill(255, 0, 100);
Sets the fill color for all shapes drawn next to bright pink: full red (255), no green (0), and moderate blue (100)
let angle = frameCount * 0.05;
Creates an angle that increases by 0.05 every frame; frameCount is a built-in p5.js variable that counts how many frames have elapsed since the sketch started
let x = width / 2 + (width / 4) * cos(angle);
Places the circle's x coordinate at the horizontal center of the canvas, then adds or subtracts a value based on cos(angle), which oscillates between -1 and 1, creating left-right movement
let y = height / 2 + (height / 4) * sin(angle);
Places the circle's y coordinate at the vertical center of the canvas, then adds or subtracts a value based on sin(angle), creating up-down movement
circle(x, y, 50);
Draws a filled circle at position (x, y) with a diameter of 50 pixels in the pink color set by fill()
if (audioStarted && osc) {
Checks two conditions: audioStarted must be true (user has clicked), and osc must exist (the oscillator has been created); only if both are true does the code inside execute
let freq = map(y, 0, height, 800, 200); // Hz
Uses p5.js's map() function to convert the circle's y position (ranging from 0 at the top to height at the bottom) into a frequency (ranging from 800 Hz at the top to 200 Hz at the bottom)
osc.frequency.rampTo(freq, 0.05);
Tells the Tone.js oscillator to smoothly transition to the target frequency over 50 milliseconds (0.05 seconds), creating a gliding pitch effect rather than an abrupt jump
if (!audioStarted) {
The exclamation mark (!) means 'not'; this block runs only when audioStarted is false, i.e., before the user clicks
fill(0);
Changes the fill color to black (0) so the instruction text will be black, overriding the pink fill() from earlier
text(..., width / 2, height * 0.1);
Draws the instruction text centered horizontally (width / 2) and near the top of the screen (10% down, or height * 0.1)

mousePressed()

mousePressed() is a p5.js built-in function that runs once every time the user clicks the mouse. Here, we use it to safely initialize Web Audio, which modern browsers require to start on user interaction. The .then() syntax ensures Tone.js is ready before we create the oscillator.

function mousePressed() {
  // Start audio context + oscillator on first user interaction
  if (!audioStarted) {
    Tone.start().then(() => {
      // Create a sine-wave oscillator, initial frequency 440 Hz
      osc = new Tone.Oscillator(440, "sine").toDestination();
      osc.start();
      audioStarted = true;
    });
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Tone.js Audio Context Initialization if (!audioStarted) { Tone.start().then(() => { osc = new Tone.Oscillator(440, "sine").toDestination(); osc.start(); audioStarted = true; }); }

Initializes the Tone.js audio context on first user click, creates a sine wave oscillator, and sets it playing

if (!audioStarted) {
Only proceeds if audioStarted is false, ensuring the oscillator is created only once on the first click
Tone.start().then(() => {
Starts the Tone.js audio context (required by Web Audio for security reasons) and waits for it to complete before running the code inside the arrow function. The .then() syntax is a Promise, a way to say 'do this next, after starting is done'
osc = new Tone.Oscillator(440, "sine").toDestination();
Creates a new sine wave oscillator at 440 Hz (a musical A note) and connects it to the speakers (.toDestination() means 'send the sound to the output')
osc.start();
Starts the oscillator playing sound
audioStarted = true;
Sets audioStarted to true, so future clicks won't try to create another oscillator and this block will never run again

windowResized()

windowResized() is a built-in p5.js function that automatically triggers whenever the browser window is resized. By calling resizeCanvas() inside it, we ensure the canvas always fills the available space and the elliptical motion scales proportionally to the screen size.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth, windowHeight);
Automatically adjusts the canvas size to match the current window width and height whenever the user resizes their browser window

📦 Key Variables

osc object

Stores the Tone.js Oscillator object once it is created. This variable holds a reference to the sine wave generator whose frequency will be controlled by the circle's y position.

let osc;
audioStarted boolean

Tracks whether the user has clicked and the audio context has been initialized. When true, the oscillator is running and its frequency is being updated; when false, the sketch displays instructions.

let audioStarted = false;
angle number

Calculated each frame as frameCount * 0.05, this variable controls the circle's position on the elliptical path. It increases continuously, causing cos(angle) and sin(angle) to oscillate and trace an ellipse.

let angle = frameCount * 0.05;
x number

The horizontal position of the circle, calculated using the ellipse formula with cosine. Ranges from (width/2 - width/4) on the left to (width/2 + width/4) on the right.

let x = width / 2 + (width / 4) * cos(angle);
y number

The vertical position of the circle, calculated using the ellipse formula with sine. This value is mapped to the oscillator frequency, so the circle's height directly controls pitch.

let y = height / 2 + (height / 4) * sin(angle);
freq number

The target frequency in Hz (cycles per second) calculated by mapping the circle's y position. Higher y values (bottom of screen) map to lower frequencies; lower y values (top) map to higher frequencies.

let freq = map(y, 0, height, 800, 200);

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG mousePressed() and draw()

If the oscillator fails to start or Tone.start() is rejected, audioStarted is never set to true, but the code still tries to update osc.frequency, which could cause errors if osc is undefined

💡 Add error handling: wrap Tone.start() in a try-catch block, or add a console.error() to catch and log failures. Also consider adding a fallback visual indicator if audio fails to initialize.

PERFORMANCE draw()

The instruction text is recreated and re-rendered every single frame (60 times per second) even though it never changes visually. This is wasteful.

💡 Use a flag or state to draw the text only once, or use a graphics buffer (createGraphics) to render it once and then display the cached image each frame.

FEATURE global scope

There is no way to stop or pause the oscillator and sound once it starts. The only way to silence it is to close the browser tab.

💡 Add a second click handler (or keypress) that stops the oscillator: osc.stop(). You could change the instruction text to 'Click to toggle sound' and add an audioStarted flag to allow restart.

STYLE draw()

The magic numbers 800 Hz, 200 Hz, and 0.05 (ramp time) are hardcoded directly in the draw loop without clear context or easy adjustment

💡 Define these as named constants at the top of the sketch: const MAX_FREQ = 800, MIN_FREQ = 200, RAMP_TIME = 0.05. This makes the code more readable and makes tuning easier.

🔄 Code Flow

Code flow showing setup, draw, mousepressed, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> ellipse_calculation[ellipse-calculation] draw --> frequency_mapping[frequency-mapping] draw --> instruction_display[instruction-display] draw --> draw setup --> windowresized[windowresized] windowresized --> resizeCanvas[Resize Canvas] mousepressed --> tone_initialization[tone-initialization] tone_initialization --> oscillator[Create Oscillator] oscillator --> startPlaying[Start Playing] ellipse_calculation --> calculatePosition[Calculate Position] frequency_mapping --> updateFrequency[Update Frequency] instruction_display --> showInstructions[Show Instructions] click setup href "#fn-setup" click draw href "#fn-draw" click ellipse_calculation href "#sub-ellipse-calculation" click frequency_mapping href "#sub-frequency-mapping" click instruction_display href "#sub-instruction-display" click windowresized href "#fn-windowresized" click tone_initialization href "#sub-tone-initialization"

Preview

Sketch 2025-11-27 02:28 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2025-11-27 02:28 - Code flow showing setup, draw, mousepressed, windowresized
Code Flow Diagram