function updateThereminSound() {
// Map mouseX -> pitch (frequency)
const freq = map(mouseX, 0, width, MIN_FREQ, MAX_FREQ, true);
// Map mouseY -> volume (top loud, bottom quiet)
const amp = map(mouseY, height, 0, 0, MAX_AMP, true);
// Smooth ramping for less clicky sound
// https://p5js.org/reference/#/p5.Oscillator/freq
// https://p5js.org/reference/#/p5.Oscillator/amp
osc.freq(freq, 0.05); // 50 ms ramp
osc.amp(amp, 0.05);
}
🔧 Subcomponents:
calculation
Pitch from Mouse X
const freq = map(mouseX, 0, width, MIN_FREQ, MAX_FREQ, true);
Converts mouse X position (0 to width) to frequency range (130 to 1200 Hz)
calculation
Volume from Mouse Y
const amp = map(mouseY, height, 0, 0, MAX_AMP, true);
Converts mouse Y position (inverted so top is loud, bottom is quiet) to amplitude (0 to 0.6)
audio-control
Apply Frequency
osc.freq(freq, 0.05);
Sets oscillator frequency with 50ms smooth ramp to avoid clicks
audio-control
Apply Amplitude
osc.amp(amp, 0.05);
Sets oscillator amplitude with 50ms smooth ramp to avoid clicks
Line by Line:
const freq = map(mouseX, 0, width, MIN_FREQ, MAX_FREQ, true);
- Uses map() to convert mouseX (0 to canvas width) into a frequency value between MIN_FREQ (130 Hz) and MAX_FREQ (1200 Hz). The 'true' parameter constrains the result to stay within bounds. Left side = low pitch, right side = high pitch.
const amp = map(mouseY, height, 0, 0, MAX_AMP, true);
- Maps mouseY to amplitude, but note the input range is 'height, 0' (reversed), so top of screen = loud (high amplitude), bottom = quiet (low amplitude). MAX_AMP is 0.6 to prevent distortion.
osc.freq(freq, 0.05);
- Sets the oscillator's frequency to the calculated value. The second parameter (0.05) is a 50-millisecond ramp time that smoothly transitions to the new frequency, preventing the 'clicky' sound from abrupt changes.
osc.amp(amp, 0.05);
- Sets the oscillator's amplitude to the calculated value with the same 50ms smooth ramp, creating a smooth volume transition as you move the mouse vertically.
function drawBackgroundGlow() {
const level = amplitudeAnalyzer.getLevel(); // 0..~0.3
const boost = constrain(level * 8, 0, 1); // exaggerate a bit
const cx = width / 2;
const cy = height * 0.5;
const maxR = min(width, height) * 0.8;
noStroke();
// A few layered circles for a soft, eerie green glow
for (let i = 5; i >= 1; i--) {
const r = (maxR * i) / 5;
const alpha = 10 + (50 * boost * i) / 5;
fill(0, 255, 120, alpha);
ellipse(cx, cy, r * 2, r * 2);
}
}
🔧 Subcomponents:
calculation
Read Audio Level
const level = amplitudeAnalyzer.getLevel();
Gets the current audio amplitude from the analyzer (0 to ~0.3)
calculation
Boost and Constrain
const boost = constrain(level * 8, 0, 1);
Multiplies level by 8 to exaggerate it, then constrains to 0-1 range for use in opacity calculations
calculation
Center Position
const cx = width / 2; const cy = height * 0.5;
Calculates the center of the canvas for the glow effect
for-loop
Layered Glow Circles
for (let i = 5; i >= 1; i--) { ... }
Draws 5 concentric circles, each with increasing opacity based on audio level
Line by Line:
const level = amplitudeAnalyzer.getLevel();
- Reads the current audio level from the analyzer. This returns a value between 0 (silent) and ~0.3 (loud), representing how much sound is currently playing.
const boost = constrain(level * 8, 0, 1);
- Multiplies the level by 8 to make it more visible (since 0.3 * 8 = 2.4), then constrains it to stay between 0 and 1. This exaggerates the visual response to audio.
const cx = width / 2; const cy = height * 0.5;
- Calculates the center point of the canvas (cx = horizontal center, cy = vertical center at 50% height).
const maxR = min(width, height) * 0.8;
- Sets the maximum radius for the glow circles to 80% of the smaller screen dimension, ensuring the glow fits on screen regardless of aspect ratio.
noStroke();
- Disables stroke (outline) for the circles so only the filled color is drawn.
for (let i = 5; i >= 1; i--)
- Loop that runs 5 times, counting down from 5 to 1. Each iteration draws one circle layer.
const r = (maxR * i) / 5;
- Calculates the radius for this layer. When i=5, r=maxR (largest circle); when i=1, r=maxR/5 (smallest circle).
const alpha = 10 + (50 * boost * i) / 5;
- Calculates opacity (alpha). Base opacity is 10, plus up to 50 more based on audio level and layer number. Outer layers (higher i) are brighter.
fill(0, 255, 120, alpha); ellipse(cx, cy, r * 2, r * 2);
- Sets the fill color to green (RGB: 0, 255, 120) with calculated alpha, then draws a circle at the center with diameter r*2.
function drawSoundWaves() {
const level = amplitudeAnalyzer.getLevel();
const levelBoost = pow(constrain(level * 5, 0, 1), 1.3);
const cx = width / 2;
const cy = height * 0.45;
const maxR = min(width, height) * 0.65;
const numWaves = 8;
const speed = 2.2;
const spacing = maxR / numWaves;
noFill();
for (let i = 0; i < numWaves; i++) {
const radius = (frameCount * speed + i * spacing) % maxR;
let alpha = map(radius, 0, maxR, 220, 0);
alpha *= 0.25 + levelBoost * 0.9; // brighter when louder
const weight = 1 + levelBoost * 4;
stroke(0, 255, 180, alpha);
strokeWeight(weight);
ellipse(cx, cy, radius * 2, radius * 2);
}
}
🔧 Subcomponents:
calculation
Read Audio Level
const level = amplitudeAnalyzer.getLevel();
Gets current audio amplitude
calculation
Boost with Power
const levelBoost = pow(constrain(level * 5, 0, 1), 1.3);
Constrains level*5 to 0-1, then raises to power 1.3 for non-linear brightness response
for-loop
Animated Wave Circles
for (let i = 0; i < numWaves; i++) { ... }
Draws 8 concentric circles that expand outward each frame, creating ripple effect
calculation
Expanding Radius
const radius = (frameCount * speed + i * spacing) % maxR;
Calculates radius based on frame count (animation), wave index, and wraps around using modulo
Line by Line:
const level = amplitudeAnalyzer.getLevel();
- Reads the current audio level (0 to ~0.3).
const levelBoost = pow(constrain(level * 5, 0, 1), 1.3);
- Multiplies level by 5, constrains to 0-1, then raises to power 1.3. The pow() function creates a non-linear response—quiet sounds have minimal effect, but loud sounds have a dramatic effect.
const cx = width / 2; const cy = height * 0.45;
- Sets the center of the sound waves slightly above the middle of the screen (45% down instead of 50%).
const maxR = min(width, height) * 0.65;
- Maximum radius is 65% of the smaller screen dimension.
const numWaves = 8; const speed = 2.2; const spacing = maxR / numWaves;
- Defines 8 wave circles, animation speed of 2.2 pixels per frame, and spacing between waves (maxR/8).
noFill();
- Disables fill so only the circle outlines are drawn.
for (let i = 0; i < numWaves; i++)
- Loop that runs 8 times, once for each wave circle.
const radius = (frameCount * speed + i * spacing) % maxR;
- Calculates the radius for this wave. frameCount increments each frame, so radius grows over time. The '% maxR' wraps it back to 0 when it exceeds maxR, creating a continuous ripple effect. Different i values create the spacing between waves.
let alpha = map(radius, 0, maxR, 220, 0);
- Maps radius to opacity: small radius (newly born waves) = 220 (bright), large radius (old waves) = 0 (invisible). This creates a fade-out effect.
alpha *= 0.25 + levelBoost * 0.9;
- Multiplies alpha by a factor based on audio level. Quiet = 0.25x opacity, loud = 1.15x opacity. This makes waves brighter when sound is louder.
const weight = 1 + levelBoost * 4;
- Calculates stroke thickness: quiet = 1 pixel, loud = up to 5 pixels. Louder sound = thicker waves.
stroke(0, 255, 180, alpha); strokeWeight(weight); ellipse(cx, cy, radius * 2, radius * 2);
- Sets stroke color to cyan-green (0, 255, 180) with calculated alpha, sets line thickness, and draws the circle.
function drawThereminBody() {
const baseY = height * 0.75;
const pitchX = width * 0.18; // left vertical antenna
const volX = width * 0.82; // right horizontal loop
// Glows behind antennas
noStroke();
fill(0, 255, 140, 40);
ellipse(pitchX, baseY - height * 0.25, width * 0.2, height * 0.5);
ellipse(volX, baseY - height * 0.15, width * 0.25, height * 0.4);
// Base box
stroke(0, 255, 150, 160);
strokeWeight(3);
fill(10, 20, 40);
const baseW = width * 0.45;
const baseH = height * 0.08;
rectMode(CENTER);
rect(width / 2, baseY + baseH * 0.1, baseW, baseH, 10);
// Vertical pitch antenna (left)
stroke(0, 255, 160);
strokeWeight(6);
const pitchTopY = baseY - height * 0.4;
line(pitchX, baseY, pitchX, pitchTopY);
// Tip sphere
noStroke();
fill(0, 255, 180);
circle(pitchX, pitchTopY, 20);
// Horizontal volume loop (right)
noFill();
stroke(0, 255, 160);
strokeWeight(6);
const loopR = width * 0.075;
ellipse(volX, baseY - height * 0.12, loopR * 2, loopR * 1.3);
// Labels
fill(180, 255, 220);
noStroke();
textAlign(CENTER, CENTER);
textSize(14);
text('PITCH (mouse X)', pitchX, baseY + baseH * 0.9);
text('VOLUME (mouse Y)', volX, baseY + baseH * 0.9);
}
🔧 Subcomponents:
calculation
Theremin Positions
const baseY = height * 0.75; const pitchX = width * 0.18; const volX = width * 0.82;
Calculates positions for the base and two antennas
drawing
Glow Effects
ellipse(pitchX, baseY - height * 0.25, width * 0.2, height * 0.5); ellipse(volX, baseY - height * 0.15, width * 0.25, height * 0.4);
Draws semi-transparent glowing ellipses behind each antenna
drawing
Base Box
rect(width / 2, baseY + baseH * 0.1, baseW, baseH, 10);
Draws the main body/base of the theremin
drawing
Pitch Antenna
line(pitchX, baseY, pitchX, pitchTopY); circle(pitchX, pitchTopY, 20);
Draws vertical antenna with sphere tip
drawing
Volume Loop
ellipse(volX, baseY - height * 0.12, loopR * 2, loopR * 1.3);
Draws horizontal ellipse loop on the right side
drawing
Text Labels
text('PITCH (mouse X)', pitchX, baseY + baseH * 0.9); text('VOLUME (mouse Y)', volX, baseY + baseH * 0.9);
Draws instructional labels below each antenna
Line by Line:
const baseY = height * 0.75;
- Sets the base of the theremin at 75% down the screen (3/4 of the way).
const pitchX = width * 0.18; const volX = width * 0.82;
- Positions the pitch antenna at 18% from left (left side) and volume antenna at 82% from left (right side), creating a symmetric layout.
noStroke(); fill(0, 255, 140, 40);
- Disables outlines and sets fill to semi-transparent green (alpha=40) for the glow effect.
ellipse(pitchX, baseY - height * 0.25, width * 0.2, height * 0.5);
- Draws a vertical ellipse glow behind the pitch antenna (20% of width wide, 50% of height tall).
ellipse(volX, baseY - height * 0.15, width * 0.25, height * 0.4);
- Draws an ellipse glow behind the volume antenna (25% of width wide, 40% of height tall).
stroke(0, 255, 150, 160); strokeWeight(3); fill(10, 20, 40);
- Sets stroke to bright green with weight 3, and fill to very dark blue-black for the base box.
rectMode(CENTER);
- Changes rectangle drawing mode so coordinates specify the center instead of top-left corner.
rect(width / 2, baseY + baseH * 0.1, baseW, baseH, 10);
- Draws the main base box at the center of the screen, slightly below baseY, with 10-pixel rounded corners.
stroke(0, 255, 160); strokeWeight(6); const pitchTopY = baseY - height * 0.4;
- Sets stroke to cyan-green with thickness 6, and calculates the top of the pitch antenna (40% of height above the base).
line(pitchX, baseY, pitchX, pitchTopY);
- Draws a vertical line from the base to the top of the antenna.
noStroke(); fill(0, 255, 180); circle(pitchX, pitchTopY, 20);
- Disables stroke and draws a 20-pixel circle at the tip of the pitch antenna in bright cyan-green.
noFill(); stroke(0, 255, 160); strokeWeight(6); const loopR = width * 0.075;
- Disables fill (outline only), sets stroke color and weight, and calculates the radius for the volume loop ellipse.
ellipse(volX, baseY - height * 0.12, loopR * 2, loopR * 1.3);
- Draws an ellipse loop on the right side (slightly wider than tall) representing the volume control antenna.
fill(180, 255, 220); noStroke(); textAlign(CENTER, CENTER); textSize(14);
- Sets text color to light cyan, disables stroke, centers text alignment, and sets font size to 14.
text('PITCH (mouse X)', pitchX, baseY + baseH * 0.9);
- Draws the label 'PITCH (mouse X)' below the pitch antenna.
text('VOLUME (mouse Y)', volX, baseY + baseH * 0.9);
- Draws the label 'VOLUME (mouse Y)' below the volume antenna.
function drawHandIndicator() {
// Simple glowing circle that follows the mouse
const level = amplitudeAnalyzer.getLevel();
const boost = constrain(level * 8, 0, 1);
const r = 10 + boost * 20;
noFill();
stroke(0, 255, 180, 200);
strokeWeight(2 + boost * 4);
ellipse(mouseX, mouseY, r * 2, r * 2);
stroke(0, 255, 120, 120);
strokeWeight(1);
ellipse(mouseX, mouseY, r * 1.3, r * 1.3);
}
🔧 Subcomponents:
calculation
Read Audio Level
const level = amplitudeAnalyzer.getLevel();
Gets current audio amplitude
calculation
Calculate Boost
const boost = constrain(level * 8, 0, 1);
Exaggerates and constrains audio level for visual effect
calculation
Dynamic Radius
const r = 10 + boost * 20;
Calculates circle radius based on audio level (10-30 pixels)
drawing
Outer Circle
ellipse(mouseX, mouseY, r * 2, r * 2);
Draws the main glowing circle
drawing
Inner Circle
ellipse(mouseX, mouseY, r * 1.3, r * 1.3);
Draws a smaller concentric circle for visual depth
Line by Line:
const level = amplitudeAnalyzer.getLevel();
- Reads the current audio level.
const boost = constrain(level * 8, 0, 1);
- Multiplies level by 8 and constrains to 0-1 for use in size calculations.
const r = 10 + boost * 20;
- Calculates the radius: quiet = 10 pixels, loud = 30 pixels. The circle grows as sound gets louder.
noFill();
- Disables fill so only the outline is drawn.
stroke(0, 255, 180, 200); strokeWeight(2 + boost * 4);
- Sets stroke color to bright cyan-green with high opacity (200), and thickness from 2 to 6 pixels based on audio level.
ellipse(mouseX, mouseY, r * 2, r * 2);
- Draws the main circle at the mouse position with diameter r*2. This follows the cursor.
stroke(0, 255, 120, 120); strokeWeight(1);
- Sets a second stroke color to dimmer green with lower opacity (120) and thin weight (1 pixel).
ellipse(mouseX, mouseY, r * 1.3, r * 1.3);
- Draws a smaller concentric circle (1.3x the radius) with the dimmer stroke, creating a layered glow effect.
function drawStartInstructions() {
push();
fill(0, 0, 0, 180);
noStroke();
rectMode(CENTER);
const w = min(width * 0.7, 500);
const h = 120;
rect(width / 2, height * 0.2, w, h, 12);
fill(180, 255, 220);
textAlign(CENTER, CENTER);
textSize(18);
text('Click or tap to awaken the Theremin\nMove mouse X for pitch, Y for volume', width / 2, height * 0.2);
pop();
}
🔧 Subcomponents:
state-management
Graphics State
push(); ... pop();
Saves and restores graphics settings so changes don't affect other functions
drawing
Semi-transparent Box
fill(0, 0, 0, 180); rect(width / 2, height * 0.2, w, h, 12);
Draws a dark semi-transparent background for the text
drawing
Instruction Text
text('Click or tap to awaken the Theremin\nMove mouse X for pitch, Y for volume', width / 2, height * 0.2);
Displays instructions to the user
Line by Line:
push();
- Saves the current graphics state (colors, stroke settings, etc.) so changes made in this function don't affect other functions.
fill(0, 0, 0, 180);
- Sets fill color to black with 180 alpha (semi-transparent) for the background box.
noStroke();
- Disables stroke so the box has no outline.
rectMode(CENTER);
- Sets rectangle mode to CENTER so coordinates specify the center point.
const w = min(width * 0.7, 500);
- Calculates box width as 70% of screen width, but caps it at 500 pixels maximum for very wide screens.
const h = 120;
- Sets box height to 120 pixels.
rect(width / 2, height * 0.2, w, h, 12);
- Draws the background box at the center horizontally, 20% down from the top, with 12-pixel rounded corners.
fill(180, 255, 220);
- Sets text color to light cyan.
textAlign(CENTER, CENTER);
- Centers text both horizontally and vertically.
textSize(18);
- Sets font size to 18 pixels.
text('Click or tap to awaken the Theremin\nMove mouse X for pitch, Y for volume', width / 2, height * 0.2);
- Draws the instruction text in the center of the box. The \n creates a line break.
pop();
- Restores the graphics state to what it was before this function, undoing all the fill, stroke, and alignment changes.