Spaces:
Running
Running
| /** | |
| * 🌿 Ivy's Creative Studio | |
| * Tab 8: p5.js Audio Visualizer | |
| * | |
| * Dedicated audio visualization with p5.js and p5.sound | |
| * 10 styles, 8 palettes, bass boost, glow, particles! 🎤🌿 | |
| */ | |
| class P5AudioRenderer { | |
| constructor() { | |
| this.p5Instance = null; | |
| this.container = null; | |
| this.isActive = false; | |
| this.audioStarted = false; | |
| // Parameters | |
| this.params = { | |
| style: "rings", | |
| sensitivity: 1.5, | |
| smoothing: 0.8, | |
| palette: "neon", | |
| bassBoost: 1.0, | |
| mirror: true, | |
| glow: false, | |
| particles: false | |
| }; | |
| } | |
| init(container) { | |
| this.container = container; | |
| } | |
| start() { | |
| this.isActive = true; | |
| this.container.classList.remove("hidden"); | |
| this.container.innerHTML = ""; | |
| this.createSketch(); | |
| } | |
| stop() { | |
| this.isActive = false; | |
| this.container.classList.add("hidden"); | |
| if (this.p5Instance) { | |
| this.p5Instance.remove(); | |
| this.p5Instance = null; | |
| } | |
| } | |
| reset() { | |
| if (this.p5Instance) { | |
| this.p5Instance.remove(); | |
| } | |
| this.createSketch(); | |
| } | |
| setStyle(style) { | |
| this.params.style = style; | |
| } | |
| setSensitivity(value) { | |
| this.params.sensitivity = value; | |
| } | |
| setSmoothing(value) { | |
| this.params.smoothing = value; | |
| } | |
| setPalette(palette) { | |
| this.params.palette = palette; | |
| } | |
| setBassBoost(value) { | |
| this.params.bassBoost = value; | |
| } | |
| setMirror(enabled) { | |
| this.params.mirror = enabled; | |
| } | |
| setGlow(enabled) { | |
| this.params.glow = enabled; | |
| } | |
| setParticles(enabled) { | |
| this.params.particles = enabled; | |
| } | |
| async startAudio() { | |
| if (this.audioStarted) return true; | |
| try { | |
| if (typeof p5 !== "undefined" && p5.prototype.getAudioContext) { | |
| const audioContext = p5.prototype.getAudioContext(); | |
| if (audioContext.state !== "running") { | |
| await audioContext.resume(); | |
| } | |
| } | |
| this.audioStarted = true; | |
| this.reset(); | |
| return true; | |
| } catch (err) { | |
| console.error("Failed to start audio:", err); | |
| return false; | |
| } | |
| } | |
| getPalette() { | |
| const palettes = { | |
| ivy: ["#22c55e", "#16a34a", "#4ade80", "#86efac", "#166534"], | |
| neon: ["#ff00ff", "#00ffff", "#ff00aa", "#00ff88", "#ffff00"], | |
| fire: ["#ff0000", "#ff4400", "#ff8800", "#ffcc00", "#ffff00"], | |
| ocean: ["#001133", "#003366", "#0066cc", "#00aaff", "#66ddff"], | |
| rainbow: ["#ff0000", "#ff8800", "#ffff00", "#00ff00", "#0088ff", "#8800ff"], | |
| synthwave: ["#ff006e", "#8338ec", "#3a86ff", "#fb5607", "#ffbe0b"], | |
| cosmic: ["#240046", "#5a189a", "#9d4edd", "#c77dff", "#e0aaff"], | |
| candy: ["#ff70a6", "#ff9770", "#ffd670", "#e9ff70", "#70d6ff"] | |
| }; | |
| return palettes[this.params.palette] || palettes.neon; | |
| } | |
| createSketch() { | |
| const self = this; | |
| const params = this.params; | |
| const sketch = p => { | |
| let fft, amplitude, mic; | |
| let spectrum = []; | |
| let waveform = []; | |
| let level = 0; | |
| let history = []; | |
| const historyLength = 60; | |
| p.setup = function () { | |
| const canvas = p.createCanvas(self.container.clientWidth, self.container.clientHeight); | |
| canvas.parent(self.container); | |
| p.colorMode(p.HSB, 360, 100, 100, 100); | |
| p.angleMode(p.DEGREES); | |
| // Initialize audio if started | |
| if (self.audioStarted) { | |
| setupAudio(); | |
| } | |
| // Initialize history | |
| for (let i = 0; i < historyLength; i++) { | |
| history.push(new Array(64).fill(0)); | |
| } | |
| }; | |
| function setupAudio() { | |
| fft = new p5.FFT(params.smoothing, 256); | |
| amplitude = new p5.Amplitude(); | |
| mic = new p5.AudioIn(); | |
| mic.start(); | |
| fft.setInput(mic); | |
| } | |
| p.draw = function () { | |
| const palette = self.getPalette(); | |
| const sensitivity = params.sensitivity; | |
| // Dark background | |
| p.background(0, 0, 5); | |
| if (!self.audioStarted || !fft) { | |
| // Show message | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| p.fill(255); | |
| p.textAlign(p.CENTER, p.CENTER); | |
| p.textSize(24); | |
| p.text('🎵 Click "Start Audio" to begin', p.width / 2, p.height / 2); | |
| p.textSize(16); | |
| p.fill(150); | |
| p.text("The visualizer will react to your microphone.", p.width / 2, p.height / 2 + 40); | |
| p.pop(); | |
| return; | |
| } | |
| // Get audio data | |
| spectrum = fft.analyze(); | |
| waveform = fft.waveform(); | |
| level = amplitude.getLevel() * sensitivity; | |
| // Store history for 3D effects | |
| const currentSpectrum = []; | |
| for (let i = 0; i < 64; i++) { | |
| currentSpectrum.push((spectrum[i * 2] / 255) * sensitivity); | |
| } | |
| history.unshift(currentSpectrum); | |
| if (history.length > historyLength) history.pop(); | |
| // Draw based on style | |
| switch (params.style) { | |
| case "rings": | |
| drawRings(palette); | |
| break; | |
| case "bars3d": | |
| drawBars3D(palette); | |
| break; | |
| case "particles": | |
| drawParticles(palette); | |
| break; | |
| case "waveform": | |
| drawWaveform(palette); | |
| break; | |
| case "spiral": | |
| drawSpiral(palette); | |
| break; | |
| case "terrain": | |
| drawTerrain(palette); | |
| break; | |
| case "ivy": | |
| drawIvy(palette); | |
| break; | |
| case "galaxy": | |
| drawGalaxy(palette); | |
| break; | |
| case "fireworks": | |
| drawFireworks(palette); | |
| break; | |
| case "kaleidoscope": | |
| drawKaleidoscope(palette); | |
| break; | |
| } | |
| // Background particles effect | |
| if (params.particles) { | |
| drawBackgroundParticles(palette); | |
| } | |
| // Glow effect | |
| if (params.glow) { | |
| applyGlowEffect(); | |
| } | |
| }; | |
| // Background particles | |
| let bgParticles = []; | |
| function drawBackgroundParticles(palette) { | |
| // Initialize if needed | |
| if (bgParticles.length === 0) { | |
| for (let i = 0; i < 50; i++) { | |
| bgParticles.push({ | |
| x: p.random(p.width), | |
| y: p.random(p.height), | |
| size: p.random(2, 6), | |
| speed: p.random(0.5, 2), | |
| color: palette[p.floor(p.random(palette.length))] | |
| }); | |
| } | |
| } | |
| for (let particle of bgParticles) { | |
| particle.y -= particle.speed * (1 + level * 3); | |
| if (particle.y < 0) { | |
| particle.y = p.height; | |
| particle.x = p.random(p.width); | |
| } | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| const c = p.color(particle.color); | |
| c.setAlpha(100 + level * 100); | |
| p.fill(c); | |
| p.noStroke(); | |
| p.ellipse(particle.x, particle.y, particle.size + level * 5); | |
| p.pop(); | |
| } | |
| } | |
| function applyGlowEffect() { | |
| p.push(); | |
| p.blendMode(p.ADD); | |
| p.filter(p.BLUR, 1); | |
| p.blendMode(p.BLEND); | |
| p.pop(); | |
| } | |
| function drawRings(palette) { | |
| p.translate(p.width / 2, p.height / 2); | |
| const numRings = 8; | |
| const maxRadius = Math.min(p.width, p.height) * 0.45; | |
| for (let ring = 0; ring < numRings; ring++) { | |
| const freqStart = ring * 8; | |
| let avgAmp = 0; | |
| for (let j = 0; j < 8; j++) { | |
| avgAmp += spectrum[freqStart + j] / 255; | |
| } | |
| avgAmp = (avgAmp / 8) * params.sensitivity; | |
| const baseRadius = ((ring + 1) / numRings) * maxRadius * 0.5; | |
| const radius = baseRadius + avgAmp * maxRadius * 0.5; | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| const c = p.color(palette[ring % palette.length]); | |
| p.noFill(); | |
| p.strokeWeight(3 + avgAmp * 10); | |
| c.setAlpha(150 + avgAmp * 100); | |
| p.stroke(c); | |
| // Pulsing ring with distortion | |
| p.beginShape(); | |
| for (let a = 0; a < 360; a += 5) { | |
| const freqIndex = Math.floor((a / 360) * 64); | |
| const amp = (spectrum[freqIndex * 2] / 255) * params.sensitivity; | |
| const r = radius + amp * 50; | |
| const x = p.cos(a) * r; | |
| const y = p.sin(a) * r; | |
| p.vertex(x, y); | |
| } | |
| p.endShape(p.CLOSE); | |
| p.pop(); | |
| } | |
| // Center glow | |
| const centerSize = 30 + level * 100; | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| for (let i = 3; i >= 0; i--) { | |
| const c = p.color(palette[0]); | |
| c.setAlpha(50 - i * 10); | |
| p.fill(c); | |
| p.noStroke(); | |
| p.ellipse(0, 0, centerSize + i * 20); | |
| } | |
| p.pop(); | |
| } | |
| function drawBars3D(palette) { | |
| const numBars = 64; | |
| const barWidth = p.width / numBars; | |
| const maxHeight = p.height * 0.7; | |
| for (let i = 0; i < numBars; i++) { | |
| const amp = (spectrum[i * 2] / 255) * params.sensitivity; | |
| const h = amp * maxHeight; | |
| const colorIndex = Math.floor(amp * palette.length); | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| const c = p.color(palette[colorIndex % palette.length]); | |
| // Main bar | |
| p.fill(c); | |
| p.noStroke(); | |
| const x = i * barWidth; | |
| const y = p.height - h; | |
| p.rect(x, y, barWidth - 2, h); | |
| // Reflection | |
| if (params.mirror) { | |
| c.setAlpha(80); | |
| p.fill(c); | |
| p.rect(x, p.height, barWidth - 2, h * 0.3); | |
| } | |
| // Top glow | |
| for (let g = 0; g < 3; g++) { | |
| c.setAlpha(50 - g * 15); | |
| p.fill(c); | |
| p.rect(x - g * 2, y - g * 2, barWidth - 2 + g * 4, 4); | |
| } | |
| p.pop(); | |
| } | |
| } | |
| function drawParticles(palette) { | |
| p.translate(p.width / 2, p.height / 2); | |
| const numParticles = 128; | |
| for (let i = 0; i < numParticles; i++) { | |
| const freqIndex = i * 2; | |
| const amp = (spectrum[freqIndex] / 255) * params.sensitivity; | |
| if (amp > 0.1) { | |
| const angle = (i / numParticles) * 360 + p.frameCount * 0.5; | |
| const radius = 50 + amp * 200; | |
| const x = p.cos(angle) * radius; | |
| const y = p.sin(angle) * radius; | |
| const size = 5 + amp * 30; | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| const c = p.color(palette[i % palette.length]); | |
| c.setAlpha(amp * 255); | |
| p.fill(c); | |
| p.noStroke(); | |
| p.ellipse(x, y, size); | |
| // Trail | |
| if (params.mirror) { | |
| c.setAlpha(amp * 100); | |
| p.fill(c); | |
| const trailX = p.cos(angle - 10) * (radius - 20); | |
| const trailY = p.sin(angle - 10) * (radius - 20); | |
| p.ellipse(trailX, trailY, size * 0.6); | |
| } | |
| p.pop(); | |
| } | |
| } | |
| // Bass circle in center | |
| const bassAmp = ((spectrum[0] + spectrum[1] + spectrum[2]) / 3 / 255) * params.sensitivity; | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| const c = p.color(palette[0]); | |
| c.setAlpha(bassAmp * 200); | |
| p.fill(c); | |
| p.noStroke(); | |
| p.ellipse(0, 0, 50 + bassAmp * 100); | |
| p.pop(); | |
| } | |
| function drawWaveform(palette) { | |
| const centerY = p.height / 2; | |
| const amplitude = p.height * 0.35 * params.sensitivity; | |
| // Draw multiple layers | |
| for (let layer = 0; layer < 3; layer++) { | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| const c = p.color(palette[layer % palette.length]); | |
| c.setAlpha(200 - layer * 50); | |
| p.stroke(c); | |
| p.strokeWeight(4 - layer); | |
| p.noFill(); | |
| p.beginShape(); | |
| for (let i = 0; i < waveform.length; i++) { | |
| const x = p.map(i, 0, waveform.length, 0, p.width); | |
| const y = centerY + waveform[i] * amplitude * (1 - layer * 0.2); | |
| p.vertex(x, y); | |
| } | |
| p.endShape(); | |
| p.pop(); | |
| } | |
| // Mirror reflection | |
| if (params.mirror) { | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| const c = p.color(palette[0]); | |
| c.setAlpha(50); | |
| p.stroke(c); | |
| p.strokeWeight(2); | |
| p.noFill(); | |
| p.beginShape(); | |
| for (let i = 0; i < waveform.length; i++) { | |
| const x = p.map(i, 0, waveform.length, 0, p.width); | |
| const y = centerY - waveform[i] * amplitude * 0.5; | |
| p.vertex(x, y); | |
| } | |
| p.endShape(); | |
| p.pop(); | |
| } | |
| // Add frequency bars at bottom | |
| const barHeight = 50; | |
| for (let i = 0; i < 32; i++) { | |
| const amp = (spectrum[i * 4] / 255) * params.sensitivity; | |
| const x = i * (p.width / 32); | |
| const h = amp * barHeight; | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| const c = p.color(palette[i % palette.length]); | |
| c.setAlpha(150); | |
| p.fill(c); | |
| p.noStroke(); | |
| p.rect(x, p.height - h, p.width / 32 - 2, h); | |
| p.pop(); | |
| } | |
| } | |
| function drawSpiral(palette) { | |
| p.translate(p.width / 2, p.height / 2); | |
| p.rotate(p.frameCount * 0.2); | |
| const numPoints = 128; | |
| const maxRadius = Math.min(p.width, p.height) * 0.4; | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| p.noFill(); | |
| for (let spiral = 0; spiral < 3; spiral++) { | |
| const c = p.color(palette[spiral % palette.length]); | |
| c.setAlpha(200); | |
| p.stroke(c); | |
| p.strokeWeight(3); | |
| p.beginShape(); | |
| for (let i = 0; i < numPoints; i++) { | |
| const angle = (i / numPoints) * 360 * 3 + spiral * 120; | |
| const baseRadius = (i / numPoints) * maxRadius; | |
| const freqIndex = i * 2; | |
| const amp = (spectrum[freqIndex] / 255) * params.sensitivity; | |
| const radius = baseRadius + amp * 50; | |
| const x = p.cos(angle) * radius; | |
| const y = p.sin(angle) * radius; | |
| p.vertex(x, y); | |
| } | |
| p.endShape(); | |
| } | |
| p.pop(); | |
| // Center pulse | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| const bassAmp = ((spectrum[0] + spectrum[1]) / 2 / 255) * params.sensitivity; | |
| const c = p.color(palette[0]); | |
| c.setAlpha(bassAmp * 255); | |
| p.fill(c); | |
| p.noStroke(); | |
| p.ellipse(0, 0, 40 + bassAmp * 60); | |
| p.pop(); | |
| } | |
| function drawTerrain(palette) { | |
| const cols = 64; | |
| const rows = historyLength; | |
| const cellWidth = p.width / cols; | |
| const cellHeight = p.height / rows; | |
| for (let y = 0; y < rows; y++) { | |
| for (let x = 0; x < cols; x++) { | |
| const amp = history[y][x]; | |
| if (amp > 0.05) { | |
| const screenX = x * cellWidth; | |
| const screenY = y * cellHeight; | |
| // Perspective effect | |
| const scale = 1 - (y / rows) * 0.5; | |
| const offsetX = (p.width / 2 - screenX) * (1 - scale); | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| const colorIndex = Math.floor(amp * palette.length); | |
| const c = p.color(palette[colorIndex % palette.length]); | |
| const alpha = (1 - y / rows) * 200 * amp; | |
| c.setAlpha(alpha); | |
| p.fill(c); | |
| p.noStroke(); | |
| const w = cellWidth * scale; | |
| const h = amp * 50 * scale; | |
| p.rect(screenX + offsetX, screenY, w - 1, h); | |
| p.pop(); | |
| } | |
| } | |
| } | |
| // Add horizontal lines for depth | |
| for (let y = 0; y < rows; y += 5) { | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| const c = p.color(palette[0]); | |
| c.setAlpha(30); | |
| p.stroke(c); | |
| p.strokeWeight(1); | |
| p.line(0, y * cellHeight, p.width, y * cellHeight); | |
| p.pop(); | |
| } | |
| } | |
| // 🌿 IVY CUTE - Version p5.js kawaii! 🎤 | |
| function drawIvy(palette) { | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| const centerX = p.width / 2; | |
| const centerY = p.height / 2; | |
| const time = p.frameCount * 0.02; | |
| // Get audio levels | |
| const bass = ((spectrum[0] + spectrum[1] + spectrum[2] + spectrum[3]) / 4 / 255) * params.sensitivity; | |
| const mid = ((spectrum[20] + spectrum[25] + spectrum[30] + spectrum[35]) / 4 / 255) * params.sensitivity; | |
| const high = ((spectrum[60] + spectrum[70] + spectrum[80] + spectrum[90]) / 4 / 255) * params.sensitivity; | |
| const faceSize = Math.min(p.width, p.height) * 0.28; | |
| // Colors | |
| const ivyGreen = p.color(34, 197, 94); | |
| const skinTone = p.color(255, 220, 195); | |
| const hairBrown = p.color(120, 80, 50); | |
| const pinkBlush = p.color(255, 180, 190); | |
| // === SOFT BACKGROUND GLOW === | |
| for (let i = 4; i > 0; i--) { | |
| p.noStroke(); | |
| p.fill(34, 197, 94, 8 + bass * 15); | |
| p.ellipse(centerX, centerY, faceSize * 3 + i * 50 + bass * 80); | |
| } | |
| // === FLOATING MUSIC NOTES (smaller, cuter) === | |
| for (let n = 0; n < 6; n++) { | |
| const noteAngle = (n / 6) * p.TWO_PI + time * 0.3; | |
| const noteRadius = faceSize * 1.8 + p.sin(time * 1.5 + n) * 20; | |
| const noteX = centerX + p.cos(noteAngle) * noteRadius; | |
| const noteY = centerY + p.sin(noteAngle) * noteRadius; | |
| const noteSize = 10 + (spectrum[n * 20] / 255) * 15 * params.sensitivity; | |
| const noteColor = p.color(palette[n % palette.length]); | |
| noteColor.setAlpha(120 + (spectrum[n * 20] / 255) * 80); | |
| p.fill(noteColor); | |
| p.noStroke(); | |
| p.ellipse(noteX, noteY, noteSize); | |
| } | |
| // === HAIR (behind face) - Soft brown waves === | |
| p.noStroke(); | |
| for (let i = 0; i < 12; i++) { | |
| const hairAngle = (i / 12) * p.PI + p.PI * 0.1; | |
| const freq = (spectrum[i * 8] / 255) * params.sensitivity; | |
| const sway = p.sin(time * 2 + i * 0.4) * 8; | |
| const hairX = centerX + p.cos(hairAngle) * (faceSize * 0.85 + sway); | |
| const hairY = centerY + p.sin(hairAngle) * (faceSize * 0.9); | |
| const hairSize = 35 + freq * 20; | |
| p.fill(hairBrown); | |
| p.ellipse(hairX, hairY, hairSize, hairSize * 1.3); | |
| } | |
| // Hair top volume | |
| p.fill(hairBrown); | |
| p.ellipse(centerX, centerY - faceSize * 0.7, faceSize * 1.6, faceSize * 0.8); | |
| // === FACE - Soft oval === | |
| p.fill(skinTone); | |
| p.noStroke(); | |
| p.ellipse(centerX, centerY, faceSize * 1.5, faceSize * 1.7); | |
| // Subtle face shading | |
| p.fill(255, 200, 170, 50); | |
| p.ellipse(centerX - faceSize * 0.3, centerY, faceSize * 0.4, faceSize * 0.8); | |
| p.ellipse(centerX + faceSize * 0.3, centerY, faceSize * 0.4, faceSize * 0.8); | |
| // === EYES - Anime style, proportionate === | |
| const eyeSpacing = faceSize * 0.3; | |
| const eyeY = centerY - faceSize * 0.1; | |
| const eyeWidth = faceSize * 0.25 + high * 8; | |
| const eyeHeight = faceSize * 0.3 + high * 10; | |
| // Eye whites | |
| p.fill(255); | |
| p.ellipse(centerX - eyeSpacing, eyeY, eyeWidth, eyeHeight); | |
| p.ellipse(centerX + eyeSpacing, eyeY, eyeWidth, eyeHeight); | |
| // Eye outline | |
| p.noFill(); | |
| p.stroke(80, 60, 50); | |
| p.strokeWeight(2); | |
| p.ellipse(centerX - eyeSpacing, eyeY, eyeWidth, eyeHeight); | |
| p.ellipse(centerX + eyeSpacing, eyeY, eyeWidth, eyeHeight); | |
| // Irises - Green! | |
| const irisSize = eyeWidth * 0.65; | |
| const lookX = p.sin(time * 0.4) * 3; | |
| const lookY = p.cos(time * 0.3) * 2; | |
| p.noStroke(); | |
| p.fill(34, 160, 80); | |
| p.ellipse(centerX - eyeSpacing + lookX, eyeY + lookY, irisSize, irisSize); | |
| p.ellipse(centerX + eyeSpacing + lookX, eyeY + lookY, irisSize, irisSize); | |
| // Pupils | |
| const pupilSize = irisSize * 0.45 + bass * 5; | |
| p.fill(20, 40, 20); | |
| p.ellipse(centerX - eyeSpacing + lookX, eyeY + lookY, pupilSize, pupilSize); | |
| p.ellipse(centerX + eyeSpacing + lookX, eyeY + lookY, pupilSize, pupilSize); | |
| // Eye sparkles ✨ | |
| p.fill(255, 255, 255, 220 + high * 35); | |
| p.ellipse(centerX - eyeSpacing - eyeWidth * 0.15, eyeY - eyeHeight * 0.15, 6, 6); | |
| p.ellipse(centerX + eyeSpacing - eyeWidth * 0.15, eyeY - eyeHeight * 0.15, 6, 6); | |
| // Secondary sparkle | |
| p.fill(255, 255, 255, 150); | |
| p.ellipse(centerX - eyeSpacing + eyeWidth * 0.1, eyeY + eyeHeight * 0.05, 3, 3); | |
| p.ellipse(centerX + eyeSpacing + eyeWidth * 0.1, eyeY + eyeHeight * 0.05, 3, 3); | |
| // === EYEBROWS === | |
| p.stroke(hairBrown); | |
| p.strokeWeight(3); | |
| p.noFill(); | |
| const browRaise = mid * 8; | |
| p.arc(centerX - eyeSpacing, eyeY - eyeHeight * 0.6 - browRaise, eyeWidth * 0.7, 12, p.PI + 0.4, p.TWO_PI - 0.4); | |
| p.arc(centerX + eyeSpacing, eyeY - eyeHeight * 0.6 - browRaise, eyeWidth * 0.7, 12, p.PI + 0.4, p.TWO_PI - 0.4); | |
| // === BLUSH - Cute rosy cheeks === | |
| p.noStroke(); | |
| const blushAlpha = 60 + high * 100; | |
| p.fill(255, 150, 160, blushAlpha); | |
| p.ellipse(centerX - eyeSpacing - eyeWidth * 0.4, eyeY + eyeHeight * 0.7, 25, 15); | |
| p.ellipse(centerX + eyeSpacing + eyeWidth * 0.4, eyeY + eyeHeight * 0.7, 25, 15); | |
| // === NOSE - Simple cute dot === | |
| p.fill(240, 180, 160); | |
| p.ellipse(centerX, centerY + faceSize * 0.1, 8, 6); | |
| // === MOUTH - Cute smile that opens gently === | |
| const mouthY = centerY + faceSize * 0.4; | |
| const mouthWidth = faceSize * 0.25 + mid * 15; | |
| const mouthOpen = 5 + bass * faceSize * 0.2; | |
| // Smile shape | |
| p.fill(180, 80, 90); | |
| p.noStroke(); | |
| if (mouthOpen > 15) { | |
| // Open mouth (singing) | |
| p.ellipse(centerX, mouthY, mouthWidth, mouthOpen); | |
| // Teeth | |
| p.fill(255); | |
| p.rect(centerX - mouthWidth * 0.35, mouthY - mouthOpen * 0.4, mouthWidth * 0.7, 6, 2); | |
| // Tongue hint | |
| if (mouthOpen > 25) { | |
| p.fill(220, 120, 130); | |
| p.ellipse(centerX, mouthY + mouthOpen * 0.2, mouthWidth * 0.5, mouthOpen * 0.3); | |
| } | |
| } else { | |
| // Closed smile | |
| p.noFill(); | |
| p.stroke(180, 80, 90); | |
| p.strokeWeight(3); | |
| p.arc(centerX, mouthY - 5, mouthWidth, 15, 0.2, p.PI - 0.2); | |
| } | |
| // Lip gloss highlight | |
| p.noStroke(); | |
| p.fill(255, 200, 210, 100); | |
| p.ellipse(centerX, mouthY - mouthOpen * 0.3, mouthWidth * 0.3, 4); | |
| // === HAIR DECORATIONS - Ivy leaves! 🌿 === | |
| // Left leaf | |
| p.push(); | |
| p.translate(centerX - faceSize * 0.9, centerY - faceSize * 0.7); | |
| p.rotate(-0.4 + p.sin(time * 1.5) * 0.1); | |
| p.fill(ivyGreen); | |
| p.noStroke(); | |
| p.ellipse(0, 0, 40 + bass * 15, 18); | |
| p.stroke(34, 150, 70); | |
| p.strokeWeight(2); | |
| p.line(-15, 0, 15, 0); | |
| // Leaf veins | |
| p.line(-8, -4, 0, 0); | |
| p.line(-8, 4, 0, 0); | |
| p.line(8, -4, 0, 0); | |
| p.line(8, 4, 0, 0); | |
| p.pop(); | |
| // Right leaf | |
| p.push(); | |
| p.translate(centerX + faceSize * 0.9, centerY - faceSize * 0.7); | |
| p.rotate(0.4 - p.sin(time * 1.5) * 0.1); | |
| p.fill(ivyGreen); | |
| p.noStroke(); | |
| p.ellipse(0, 0, 40 + bass * 15, 18); | |
| p.stroke(34, 150, 70); | |
| p.strokeWeight(2); | |
| p.line(-15, 0, 15, 0); | |
| p.line(-8, -4, 0, 0); | |
| p.line(-8, 4, 0, 0); | |
| p.line(8, -4, 0, 0); | |
| p.line(8, 4, 0, 0); | |
| p.pop(); | |
| // === SOUND WAVES (subtle) === | |
| if (bass > 0.2) { | |
| for (let w = 0; w < 3; w++) { | |
| const waveProgress = (time * 1.5 + w * 0.33) % 1; | |
| const waveRadius = 20 + waveProgress * 60; | |
| const waveAlpha = (1 - waveProgress) * 100 * bass; | |
| p.noFill(); | |
| p.stroke(255, 180, 200, waveAlpha); | |
| p.strokeWeight(2); | |
| p.arc(centerX, mouthY, waveRadius * 2, waveRadius, 0.3, p.PI - 0.3); | |
| } | |
| } | |
| // === NAME TAG === | |
| p.noStroke(); | |
| p.fill(34, 197, 94); | |
| p.textSize(16); | |
| p.textAlign(p.CENTER, p.CENTER); | |
| p.text("🌿 Ivy", centerX, centerY + faceSize * 1.5); | |
| p.pop(); | |
| } | |
| // === NEW STYLES === | |
| let galaxyStars = []; | |
| function drawGalaxy(palette) { | |
| p.translate(p.width / 2, p.height / 2); | |
| // Initialize stars | |
| if (galaxyStars.length === 0) { | |
| for (let i = 0; i < 200; i++) { | |
| const angle = p.random(p.TWO_PI); | |
| const radius = p.random(50, Math.min(p.width, p.height) * 0.45); | |
| galaxyStars.push({ | |
| angle: angle, | |
| radius: radius, | |
| speed: p.random(0.001, 0.005), | |
| size: p.random(1, 4), | |
| color: palette[p.floor(p.random(palette.length))] | |
| }); | |
| } | |
| } | |
| const bassBoost = params.bassBoost; | |
| let bassLevel = 0; | |
| for (let i = 0; i < 10; i++) { | |
| bassLevel += spectrum[i] / 255; | |
| } | |
| bassLevel = (bassLevel / 10) * params.sensitivity * bassBoost; | |
| // Rotate and draw stars | |
| for (let star of galaxyStars) { | |
| star.angle += star.speed * (1 + level * 2); | |
| const spiralOffset = star.radius * 0.02; | |
| const x = p.cos(star.angle + spiralOffset) * (star.radius + bassLevel * 30); | |
| const y = p.sin(star.angle + spiralOffset) * (star.radius + bassLevel * 30) * 0.6; | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| const c = p.color(star.color); | |
| c.setAlpha(150 + level * 100); | |
| p.fill(c); | |
| p.noStroke(); | |
| p.ellipse(x, y, star.size + bassLevel * 3); | |
| p.pop(); | |
| } | |
| // Center glow | |
| for (let i = 5; i >= 0; i--) { | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| const c = p.color(palette[0]); | |
| c.setAlpha(30 - i * 5); | |
| p.fill(c); | |
| p.noStroke(); | |
| p.ellipse(0, 0, 50 + i * 30 + bassLevel * 50); | |
| p.pop(); | |
| } | |
| } | |
| let fireworksParticles = []; | |
| function drawFireworks(palette) { | |
| const bassBoost = params.bassBoost; | |
| // Launch new firework on bass hit | |
| let bassLevel = 0; | |
| for (let i = 0; i < 10; i++) { | |
| bassLevel += spectrum[i] / 255; | |
| } | |
| bassLevel = (bassLevel / 10) * params.sensitivity * bassBoost; | |
| if (bassLevel > 0.5 && p.random() > 0.7) { | |
| const x = p.random(p.width * 0.2, p.width * 0.8); | |
| const y = p.random(p.height * 0.2, p.height * 0.5); | |
| const color = palette[p.floor(p.random(palette.length))]; | |
| for (let i = 0; i < 30; i++) { | |
| const angle = p.random(p.TWO_PI); | |
| const speed = p.random(2, 8); | |
| fireworksParticles.push({ | |
| x: x, | |
| y: y, | |
| vx: p.cos(angle) * speed, | |
| vy: p.sin(angle) * speed, | |
| life: 1, | |
| color: color, | |
| size: p.random(3, 8) | |
| }); | |
| } | |
| } | |
| // Update and draw particles | |
| for (let i = fireworksParticles.length - 1; i >= 0; i--) { | |
| const particle = fireworksParticles[i]; | |
| particle.x += particle.vx; | |
| particle.y += particle.vy; | |
| particle.vy += 0.1; // Gravity | |
| particle.life -= 0.015; | |
| if (particle.life <= 0) { | |
| fireworksParticles.splice(i, 1); | |
| continue; | |
| } | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| const c = p.color(particle.color); | |
| c.setAlpha(particle.life * 255); | |
| p.fill(c); | |
| p.noStroke(); | |
| p.ellipse(particle.x, particle.y, particle.size * particle.life); | |
| p.pop(); | |
| // Trail | |
| if (particle.life > 0.5) { | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| const tc = p.color(particle.color); | |
| tc.setAlpha(particle.life * 100); | |
| p.stroke(tc); | |
| p.strokeWeight(1); | |
| p.line(particle.x, particle.y, particle.x - particle.vx * 2, particle.y - particle.vy * 2); | |
| p.pop(); | |
| } | |
| } | |
| } | |
| function drawKaleidoscope(palette) { | |
| p.translate(p.width / 2, p.height / 2); | |
| const segments = 12; | |
| const bassBoost = params.bassBoost; | |
| for (let seg = 0; seg < segments; seg++) { | |
| p.push(); | |
| p.rotate((seg / segments) * p.TWO_PI); | |
| if (seg % 2 === 1) { | |
| p.scale(-1, 1); | |
| } | |
| // Draw frequency bars in each segment | |
| const barsPerSegment = 16; | |
| for (let i = 0; i < barsPerSegment; i++) { | |
| const freqIndex = i * 4; | |
| let amp = (spectrum[freqIndex] / 255) * params.sensitivity; | |
| // Apply bass boost to low frequencies | |
| if (i < 4) amp *= bassBoost; | |
| const barWidth = 10; | |
| const barHeight = amp * 150; | |
| const x = 50 + i * 12; | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| const c = p.color(palette[(seg + i) % palette.length]); | |
| c.setAlpha(180); | |
| p.fill(c); | |
| p.noStroke(); | |
| // Triangular shape | |
| p.beginShape(); | |
| p.vertex(x, 0); | |
| p.vertex(x + barWidth, 0); | |
| p.vertex(x + barWidth / 2, -barHeight); | |
| p.endShape(p.CLOSE); | |
| p.pop(); | |
| } | |
| p.pop(); | |
| } | |
| // Center mandala | |
| const centerSize = 40 + level * 30; | |
| for (let i = 3; i >= 0; i--) { | |
| p.push(); | |
| p.colorMode(p.RGB); | |
| const c = p.color(palette[i % palette.length]); | |
| c.setAlpha(150 - i * 30); | |
| p.fill(c); | |
| p.noStroke(); | |
| p.ellipse(0, 0, centerSize + i * 15); | |
| p.pop(); | |
| } | |
| } | |
| p.windowResized = function () { | |
| p.resizeCanvas(self.container.clientWidth, self.container.clientHeight); | |
| }; | |
| }; | |
| this.p5Instance = new p5(sketch); | |
| } | |
| dispose() { | |
| this.stop(); | |
| } | |
| } | |
| // Export | |
| window.P5AudioRenderer = P5AudioRenderer; | |