π€ AI
Hereβs a complete Theremin simulator using p5.js + p5.sound, with:
- mouse X β pitch (frequency)
- mouse Y β volume (amplitude)
- p5.Oscillator using a sine wave
- vertical pitch antenna (left), horizontal volume loop (right)
- pulsing green sound waves on a dark background
- eerie green glow that reacts to the sound
Audio will start after the first click/tap (required by browsers).
---
### `index.html`
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>p5.js Theremin Simulator</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- p5 core -->
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.3/lib/p5.min.js"></script>
<!-- p5.sound addon (must load after p5.js) -->
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.3/lib/addons/p5.sound.min.js"></script>
<!-- Your sketch -->
<script src="sketch.js"></script>
</body>
</html>
```
---
### `sketch.js`
```js
// Theremin Simulator using p5.js + p5.sound
// mouseX -> pitch, mouseY -> volume
// p5.Oscillator docs: https://p5js.org/reference/#/p5.Oscillator
let osc;
let amplitudeAnalyzer;
let audioStarted = false;
// Sound range
const MIN_FREQ = 130; // low pitch (Hz)
const MAX_FREQ = 1200; // high pitch (Hz)
const MAX_AMP = 0.6; // max amplitude (0..1)
function setup() {
createCanvas(windowWidth, windowHeight);
// Create sine-wave oscillator (but don't start yet)
osc = new p5.Oscillator('sine');
osc.amp(0); // ensure silent until we control it
// Amplitude analyzer for visual pulsing
// p5.Amplitude docs: https://p5js.org/reference/#/p5.Amplitude
amplitudeAnalyzer = new p5.Amplitude(0.9); // smoothing
amplitudeAnalyzer.setInput(osc);
textFont('sans-serif');
}
function draw() {
background(3, 5, 15); // very dark blue/black
// Update sound based on mouse if audio is running
if (audioStarted) {
updateThereminSound();
}
// Draw eerie glow and sound waves that react to audio
drawBackgroundGlow();
drawSoundWaves();
// Draw the theremin hardware (antennas, base, labels)
drawThereminBody();
// Small visual crosshair where the "hand" is
drawHandIndicator();
// Instruction overlay before first click
if (!audioStarted) {
drawStartInstructions();
}
}
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);
}
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);
}
}
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);
}
}
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);
}
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);
}
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();
}
// Start audio context + oscillator on first user interaction
// userStartAudio docs: https://p5js.org/reference/#/p5/userStartAudio
function mousePressed() {
if (!audioStarted) {
userStartAudio();
osc.start();
audioStarted = true;
}
}
function touchStarted() {
// Mirror mousePressed behavior for touch screens
mousePressed();
return false; // prevent default scrolling on mobile
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
}
```
---
### `style.css`
```css
body {
margin: 0;
padding: 0;
overflow: hidden;
background: #020309; /* fallback background behind canvas */
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
}
canvas {
display: block;
}
```
---
You can tweak:
- `MIN_FREQ` / `MAX_FREQ` to change the pitch range
- `MAX_AMP` to make it louder/softer
- Colors and glow strength in `drawBackgroundGlow()` / `drawSoundWaves()`
If youβd like, I can add a small on-screen readout of the current frequency and volume, or quantize the pitch to musical notes.