/** * 🌿 Ivy's Creative Studio * Tab 5: Audio Visualizer * * Web Audio API + WebGPU for sound-reactive visuals * Now with 10 styles and 8 palettes! 🎤🌿 */ class AudioRenderer { constructor() { this.device = null; this.context = null; this.format = null; // Audio parameters this.params = { source: "mic", style: 4, // 0=bars, 1=circular, 2=waveform, 3=spectrum, 4=ivy, 5=galaxy, 6=dna, 7=fireworks, 8=rings, 9=particles palette: 0, // 0=ivy, 1=rainbow, 2=fire, 3=ocean, 4=neon, 5=synthwave, 6=cosmic, 7=candy sensitivity: 1.0, smoothing: 0.8, bassBoost: 1.0, glow: true, mirror: false }; // Audio state this.audioContext = null; this.analyser = null; this.frequencyData = null; this.timeDomainData = null; this.audioSource = null; this.isAudioStarted = false; this.input = null; this.animationLoop = null; this.isActive = false; this.time = 0; } async init(device, context, format, canvas) { this.device = device; this.context = context; this.format = format; this.canvas = canvas; // Create shader const shaderModule = device.createShaderModule({ label: "Audio Shader", code: this.getShaderCode() }); // Create uniform buffer - increased for new params this.uniformBuffer = device.createBuffer({ label: "Audio Uniforms", size: 80, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); // Create audio data buffer (128 frequency bins) this.audioDataBuffer = device.createBuffer({ label: "Audio Data Buffer", size: 128 * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); // Create waveform buffer this.waveformBuffer = device.createBuffer({ label: "Waveform Buffer", size: 256 * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); // Create bind group layout const bindGroupLayout = device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, { binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } }, { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } } ] }); // Create pipeline this.pipeline = device.createRenderPipeline({ label: "Audio Pipeline", layout: device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }), vertex: { module: shaderModule, entryPoint: "vertexMain" }, fragment: { module: shaderModule, entryPoint: "fragmentMain", targets: [{ format }] }, primitive: { topology: "triangle-list" } }); // Create bind group this.bindGroup = device.createBindGroup({ layout: bindGroupLayout, entries: [ { binding: 0, resource: { buffer: this.uniformBuffer } }, { binding: 1, resource: { buffer: this.audioDataBuffer } }, { binding: 2, resource: { buffer: this.waveformBuffer } } ] }); // Input handler this.input = new WebGPUUtils.InputHandler(canvas); // Animation loop this.animationLoop = new WebGPUUtils.AnimationLoop((deltaTime, totalTime) => { this.time = totalTime; this.updateAudioData(); this.render(); }); // Initialize with zeros const zeros = new Float32Array(128); this.device.queue.writeBuffer(this.audioDataBuffer, 0, zeros); const waveZeros = new Float32Array(256); for (let i = 0; i < 256; i++) waveZeros[i] = 0.5; this.device.queue.writeBuffer(this.waveformBuffer, 0, waveZeros); } async startAudio() { if (this.isAudioStarted) return true; try { this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); this.analyser = this.audioContext.createAnalyser(); this.analyser.fftSize = 256; this.analyser.smoothingTimeConstant = this.params.smoothing; this.frequencyData = new Uint8Array(this.analyser.frequencyBinCount); this.timeDomainData = new Uint8Array(this.analyser.fftSize); if (this.params.source === "mic") { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); this.audioSource = this.audioContext.createMediaStreamSource(stream); this.audioSource.connect(this.analyser); } this.isAudioStarted = true; return true; } catch (err) { console.error("Failed to start audio:", err); return false; } } async loadAudioFile(file) { if (!this.audioContext) { this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); this.analyser = this.audioContext.createAnalyser(); this.analyser.fftSize = 256; this.analyser.smoothingTimeConstant = this.params.smoothing; this.frequencyData = new Uint8Array(this.analyser.frequencyBinCount); this.timeDomainData = new Uint8Array(this.analyser.fftSize); } const arrayBuffer = await file.arrayBuffer(); const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer); if (this.audioSource) { this.audioSource.disconnect(); } this.audioSource = this.audioContext.createBufferSource(); this.audioSource.buffer = audioBuffer; this.audioSource.connect(this.analyser); this.analyser.connect(this.audioContext.destination); this.audioSource.start(0); this.isAudioStarted = true; return true; } stopAudio() { if (this.audioSource) { try { this.audioSource.disconnect(); } catch (e) {} } if (this.audioContext) { this.audioContext.close(); this.audioContext = null; } this.isAudioStarted = false; } updateAudioData() { if (!this.isAudioStarted || !this.analyser) { const zeros = new Float32Array(128); this.device.queue.writeBuffer(this.audioDataBuffer, 0, zeros); const waveZeros = new Float32Array(256); for (let i = 0; i < 256; i++) waveZeros[i] = 0.5; this.device.queue.writeBuffer(this.waveformBuffer, 0, waveZeros); return; } this.analyser.getByteFrequencyData(this.frequencyData); const audioData = new Float32Array(128); for (let i = 0; i < 128; i++) { audioData[i] = (this.frequencyData[i] / 255.0) * this.params.sensitivity; } this.device.queue.writeBuffer(this.audioDataBuffer, 0, audioData); this.analyser.getByteTimeDomainData(this.timeDomainData); const waveformData = new Float32Array(256); for (let i = 0; i < 256; i++) { waveformData[i] = this.timeDomainData[i] / 255.0; } this.device.queue.writeBuffer(this.waveformBuffer, 0, waveformData); } start() { this.isActive = true; this.animationLoop.start(); } stop() { this.isActive = false; this.animationLoop.stop(); } setSource(source) { this.params.source = source; } setStyle(style) { const styles = { bars: 0, circular: 1, waveform: 2, spectrum: 3, ivy: 4, galaxy: 5, dna: 6, fireworks: 7, rings: 8, particles: 9 }; this.params.style = styles[style] ?? 4; } setPalette(palette) { const palettes = { ivy: 0, rainbow: 1, fire: 2, ocean: 3, neon: 4, synthwave: 5, cosmic: 6, candy: 7 }; this.params.palette = palettes[palette] ?? 0; } setSensitivity(value) { this.params.sensitivity = value; } setSmoothing(value) { this.params.smoothing = value; if (this.analyser) { this.analyser.smoothingTimeConstant = value; } } setBassBoost(value) { this.params.bassBoost = value; } setGlow(enabled) { this.params.glow = enabled; } setMirror(enabled) { this.params.mirror = enabled; } updateUniforms() { const aspect = this.canvas.width / this.canvas.height; const data = new Float32Array([ this.params.style, // 0 this.params.palette, // 4 this.params.sensitivity, // 8 this.time, // 12 aspect, // 16 this.input.mouseX, // 20 this.input.mouseY, // 24 this.isAudioStarted ? 1.0 : 0.0, // 28 this.params.bassBoost, // 32 this.params.glow ? 1.0 : 0.0, // 36 this.params.mirror ? 1.0 : 0.0, // 40 0.0, // padding 0.0, 0.0, 0.0, 0.0 // padding ]); this.device.queue.writeBuffer(this.uniformBuffer, 0, data); } render() { if (!this.isActive) return; WebGPUUtils.resizeCanvasToDisplaySize(this.canvas, window.devicePixelRatio); this.updateUniforms(); const commandEncoder = this.device.createCommandEncoder(); const renderPass = commandEncoder.beginRenderPass({ colorAttachments: [ { view: this.context.getCurrentTexture().createView(), clearValue: { r: 0.02, g: 0.02, b: 0.05, a: 1 }, loadOp: "clear", storeOp: "store" } ] }); renderPass.setPipeline(this.pipeline); renderPass.setBindGroup(0, this.bindGroup); renderPass.draw(3); renderPass.end(); this.device.queue.submit([commandEncoder.finish()]); } getShaderCode() { return /* wgsl */ ` struct Uniforms { style: f32, palette: f32, sensitivity: f32, time: f32, aspect: f32, mouseX: f32, mouseY: f32, audioStarted: f32, bassBoost: f32, doGlow: f32, doMirror: f32, } @group(0) @binding(0) var u: Uniforms; @group(0) @binding(1) var audioData: array; @group(0) @binding(2) var waveform: array; struct VertexOutput { @builtin(position) position: vec4f, @location(0) uv: vec2f, } @vertex fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { var pos = array( vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0) ); var output: VertexOutput; output.position = vec4f(pos[vertexIndex], 0.0, 1.0); output.uv = pos[vertexIndex] * 0.5 + 0.5; return output; } // Palette function fn getPaletteColor(t: f32, paletteId: i32) -> vec3f { let tt = fract(t); // Ivy Green if (paletteId == 0) { return vec3f(0.1 + 0.3 * tt, 0.5 + 0.5 * tt, 0.2 + 0.3 * tt); } // Rainbow else if (paletteId == 1) { return vec3f( 0.5 + 0.5 * cos(6.28318 * (tt + 0.0)), 0.5 + 0.5 * cos(6.28318 * (tt + 0.33)), 0.5 + 0.5 * cos(6.28318 * (tt + 0.67)) ); } // Fire else if (paletteId == 2) { return vec3f(min(1.0, tt * 2.0), tt * tt, tt * tt * tt * 0.5); } // Ocean else if (paletteId == 3) { return vec3f(0.0 + 0.2 * tt, 0.3 + 0.4 * tt, 0.6 + 0.4 * tt); } // Neon else if (paletteId == 4) { return vec3f( 0.5 + 0.5 * sin(tt * 12.56), 0.5 + 0.5 * sin(tt * 12.56 + 2.094), 0.5 + 0.5 * sin(tt * 12.56 + 4.188) ); } // Synthwave else if (paletteId == 5) { return vec3f(0.8 + 0.2 * tt, 0.2 + 0.3 * tt, 0.7 + 0.3 * tt); } // Cosmic else if (paletteId == 6) { return vec3f( 0.1 + 0.4 * sin(tt * 6.28), 0.05 + 0.2 * sin(tt * 6.28 + 2.0), 0.4 + 0.6 * sin(tt * 6.28 + 4.0) ); } // Candy else { return vec3f( 0.9 + 0.1 * sin(tt * 12.56), 0.5 + 0.4 * sin(tt * 12.56 + 2.0), 0.8 + 0.2 * sin(tt * 12.56 + 4.0) ); } } fn getFrequency(index: i32) -> f32 { let i = clamp(index, 0, 127); return audioData[i]; } fn getWaveform(index: i32) -> f32 { let i = clamp(index, 0, 255); return waveform[i]; } fn getBass() -> f32 { return (getFrequency(0) + getFrequency(1) + getFrequency(2) + getFrequency(3)) * 0.25 * u.bassBoost; } fn getMid() -> f32 { return (getFrequency(20) + getFrequency(25) + getFrequency(30) + getFrequency(35)) * 0.25; } fn getHigh() -> f32 { return (getFrequency(60) + getFrequency(70) + getFrequency(80) + getFrequency(90)) * 0.25; } fn sdCircle(p: vec2f, r: f32) -> f32 { return length(p) - r; } fn sdEllipse(p: vec2f, rx: f32, ry: f32) -> f32 { let k = length(p / vec2f(rx, ry)); return (k - 1.0) * min(rx, ry); } fn barsVisualization(uv: vec2f, paletteId: i32) -> vec3f { let barCount = 64; let barWidth = 1.0 / f32(barCount); let barIndex = i32(uv.x * f32(barCount)); let freqIndex = barIndex * 2; let amplitude = getFrequency(freqIndex); let barHeight = amplitude; let inBar = uv.y < barHeight && uv.x > f32(barIndex) * barWidth && uv.x < f32(barIndex + 1) * barWidth - 0.002; if (inBar) { let hue = f32(barIndex) / f32(barCount); return getPaletteColor(hue + amplitude * 0.3, paletteId) * (0.5 + amplitude); } return vec3f(0.02, 0.02, 0.05); } fn circularVisualization(uv: vec2f, paletteId: i32) -> vec3f { var p = (uv - 0.5) * 2.0; p.x *= u.aspect; let r = length(p); let a = atan2(p.y, p.x); let freqIndex = i32((a / 6.28318 + 0.5) * 64.0); let amplitude = getFrequency(freqIndex); let baseRadius = 0.3; let maxRadius = baseRadius + amplitude * 0.4; let dist = abs(r - maxRadius); let glow = exp(-dist * 20.0) * amplitude; let hue = (a / 6.28318 + 0.5) + u.time * 0.1; let color = getPaletteColor(hue, paletteId); let innerGlow = exp(-r * 3.0) * 0.2; return color * glow + getPaletteColor(0.5, paletteId) * innerGlow * 0.3; } fn waveformVisualization(uv: vec2f, paletteId: i32) -> vec3f { let waveIndex = i32(uv.x * 256.0); let waveValue = getWaveform(waveIndex); let y = uv.y; let waveY = waveValue; let dist = abs(y - waveY); let thickness = 0.01; if (dist < thickness) { let intensity = 1.0 - dist / thickness; let hue = uv.x + u.time * 0.2; return getPaletteColor(hue, paletteId) * intensity; } let glow = exp(-dist * 30.0) * 0.5; return getPaletteColor(0.5, paletteId) * glow * 0.5; } fn spectrumVisualization(uv: vec2f, paletteId: i32) -> vec3f { var p = (uv - 0.5) * 2.0; p.x *= u.aspect; var color = vec3f(0.0); for (var i = 0; i < 8; i++) { let freqIndex = i * 8 + 4; let amplitude = getFrequency(freqIndex); let radius = 0.1 + f32(i) * 0.1; let r = length(p); let targetR = radius + amplitude * 0.15; let dist = abs(r - targetR); let glow = exp(-dist * 40.0) * amplitude; let hue = f32(i) / 8.0 + u.time * 0.1; color += getPaletteColor(hue, paletteId) * glow; } return color; } // 🌿 IVY CUTE AVATAR! 🎤 (Version kawaii) fn ivyVisualization(uv: vec2f) -> vec3f { var p = (uv - 0.5) * 2.0; p.x *= u.aspect; var color = vec3f(0.02, 0.04, 0.06); // Dark blue-ish background let bass = getBass(); let mid = getMid(); let high = getHigh(); let ivyGreen = vec3f(0.13, 0.77, 0.37); let softPink = vec3f(1.0, 0.7, 0.75); let warmBrown = vec3f(0.45, 0.3, 0.2); // === HAIR (behind face) - Soft brown waves === for (var h = 0; h < 8; h++) { let hairAngle = f32(h) / 8.0 * 3.14159 - 0.3; let freq = getFrequency(h * 10); let waveOffset = sin(u.time * 2.0 + f32(h)) * 0.03; let hairX = cos(hairAngle) * 0.5 + waveOffset; let hairY = sin(hairAngle) * 0.55 + 0.1; let hairDist = sdCircle(p - vec2f(hairX, hairY), 0.15 + freq * 0.05); let hairGlow = exp(-hairDist * 8.0); color += warmBrown * hairGlow * 0.6; } // === FACE - Soft oval shape === let faceWidth = 0.38; let faceHeight = 0.45; let faceDist = sdEllipse(p - vec2f(0.0, -0.02), faceWidth, faceHeight); // Face fill - peachy skin tone if (faceDist < 0.0) { let skinColor = vec3f(1.0, 0.85, 0.75); color = skinColor; // Subtle face shading let shade = 1.0 - abs(p.x) * 0.3; color *= shade; } // Face outline glow let faceGlow = exp(-abs(faceDist) * 25.0); color += vec3f(0.9, 0.7, 0.65) * faceGlow * 0.3; // === EYES - Anime style, SMALLER and cuter === let eyeSpacing = 0.13; let eyeY = 0.02; let eyeWidth = 0.07 + high * 0.01; let eyeHeight = 0.09 + high * 0.015; let leftEyePos = p - vec2f(-eyeSpacing, eyeY); let rightEyePos = p - vec2f(eyeSpacing, eyeY); let leftEyeDist = sdEllipse(leftEyePos, eyeWidth, eyeHeight); let rightEyeDist = sdEllipse(rightEyePos, eyeWidth, eyeHeight); // Eye whites if (leftEyeDist < 0.0 || rightEyeDist < 0.0) { color = vec3f(1.0, 1.0, 1.0); } // Irises - Green like ivy! let irisSize = eyeWidth * 0.7; let lookOffset = vec2f(sin(u.time * 0.5) * 0.01, cos(u.time * 0.3) * 0.005); let leftIrisDist = sdCircle(leftEyePos - lookOffset, irisSize); let rightIrisDist = sdCircle(rightEyePos - lookOffset, irisSize); if (leftIrisDist < 0.0 || rightIrisDist < 0.0) { color = ivyGreen * 0.8; } // Pupils - smaller let pupilSize = irisSize * 0.5 + bass * 0.01; let leftPupilDist = sdCircle(leftEyePos - lookOffset, pupilSize); let rightPupilDist = sdCircle(rightEyePos - lookOffset, pupilSize); if (leftPupilDist < 0.0 || rightPupilDist < 0.0) { color = vec3f(0.1, 0.15, 0.1); } // Eye sparkles ✨ - cute anime style let sparklePos1 = vec2f(-0.02, 0.02); let sparkle1L = sdCircle(leftEyePos - sparklePos1, 0.012); let sparkle1R = sdCircle(rightEyePos - sparklePos1, 0.012); let sparkle2L = sdCircle(leftEyePos - vec2f(0.015, -0.01), 0.006); let sparkle2R = sdCircle(rightEyePos - vec2f(0.015, -0.01), 0.006); let sparkleIntensity = 0.8 + high * 0.5; if (sparkle1L < 0.0 || sparkle1R < 0.0 || sparkle2L < 0.0 || sparkle2R < 0.0) { color = vec3f(1.0, 1.0, 1.0) * sparkleIntensity; } // === BLUSH - Cute rosy cheeks === let blushY = -0.05; let leftBlush = sdEllipse(p - vec2f(-0.22, blushY), 0.06, 0.035); let rightBlush = sdEllipse(p - vec2f(0.22, blushY), 0.06, 0.035); let blushAmt = exp(-leftBlush * 20.0) + exp(-rightBlush * 20.0); color += softPink * blushAmt * (0.3 + high * 0.4); // === MOUTH - Cute smile that opens with music === let mouthY = -0.15; let smileWidth = 0.08 + mid * 0.03; let mouthOpen = 0.02 + bass * 0.06; // Opens gently with bass let mouthPos = p - vec2f(0.0, mouthY); // Smile curve (arc shape when closed, oval when open) let smileDist = sdEllipse(mouthPos, smileWidth, mouthOpen); if (smileDist < 0.0 && mouthPos.y < 0.01) { color = vec3f(0.6, 0.2, 0.25); // Mouth interior } // Lips - soft pink arc let lipGlow = exp(-abs(smileDist) * 30.0); if (mouthPos.y < 0.02) { color += softPink * lipGlow * 0.5; } // Little tooth showing when singing if (bass > 0.3 && mouthOpen > 0.04) { let toothDist = sdEllipse(mouthPos - vec2f(0.0, 0.015), 0.02, 0.015); if (toothDist < 0.0) { color = vec3f(1.0, 1.0, 0.98); } } // === EYEBROWS - Expressive === let browY = eyeY + eyeHeight + 0.03; let browRaise = mid * 0.02; let leftBrowPos = p - vec2f(-eyeSpacing, browY + browRaise); let rightBrowPos = p - vec2f(eyeSpacing, browY + browRaise); let leftBrowDist = sdEllipse(leftBrowPos, 0.05, 0.012); let rightBrowDist = sdEllipse(rightBrowPos, 0.05, 0.012); let browGlow = exp(-leftBrowDist * 40.0) + exp(-rightBrowDist * 40.0); color += warmBrown * browGlow * 0.8; // === HAIR DECORATIONS - Ivy leaves! 🌿 === let leaf1Pos = p - vec2f(-0.35, 0.35); let leaf1Rot = leaf1Pos * mat2x2f(0.8, -0.6, 0.6, 0.8); let leaf1Dist = sdEllipse(leaf1Rot, 0.08, 0.03); let leaf2Pos = p - vec2f(0.38, 0.32); let leaf2Rot = leaf2Pos * mat2x2f(0.8, 0.6, -0.6, 0.8); let leaf2Dist = sdEllipse(leaf2Rot, 0.07, 0.025); let leafGlow = exp(-leaf1Dist * 25.0) + exp(-leaf2Dist * 25.0); color += ivyGreen * leafGlow * (0.7 + bass * 0.5); // === FLOATING MUSICAL NOTES === for (var n = 0; n < 5; n++) { let noteAngle = f32(n) / 5.0 * 6.28318 + u.time * 0.4; let noteRadius = 0.6 + sin(u.time + f32(n)) * 0.08; let notePos = vec2f(cos(noteAngle), sin(noteAngle)) * noteRadius; let freq = getFrequency(n * 25); let noteDist = sdCircle(p - notePos, 0.02 + freq * 0.015); let noteGlow = exp(-noteDist * 35.0) * freq; let noteHue = f32(n) / 5.0 + u.time * 0.1; color += vec3f( 0.5 + 0.5 * sin(noteHue * 6.28318), 0.5 + 0.5 * sin(noteHue * 6.28318 + 2.094), 0.5 + 0.5 * sin(noteHue * 6.28318 + 4.188) ) * noteGlow * 0.8; } // === SOUND WAVES emanating === if (bass > 0.15) { for (var w = 0; w < 3; w++) { let waveTime = fract(u.time * 0.8 + f32(w) * 0.33); let waveRadius = 0.5 + waveTime * 0.4; let waveDist = abs(length(p) - waveRadius); let waveGlow = exp(-waveDist * 40.0) * (1.0 - waveTime) * bass; color += ivyGreen * waveGlow * 0.3; } } return color; } // Galaxy fn galaxyVisualization(uv: vec2f, paletteId: i32) -> vec3f { var p = (uv - 0.5) * 2.0; p.x *= u.aspect; var color = vec3f(0.01, 0.01, 0.03); let bass = getBass(); let r = length(p); let a = atan2(p.y, p.x); for (var arm = 0; arm < 3; arm++) { let armAngle = f32(arm) * 2.094 + u.time * 0.2; let spiral = a - armAngle + r * 3.0; let armDist = abs(sin(spiral * 2.0)) * 0.3; let freqIndex = i32(r * 64.0) + arm * 20; let freq = getFrequency(freqIndex); let armGlow = exp(-armDist * 10.0) * exp(-r * 2.0) * (0.5 + freq); let hue = f32(arm) / 3.0 + r * 0.5; color += getPaletteColor(hue, paletteId) * armGlow; } let centerGlow = exp(-r * 5.0) * (1.0 + bass); color += vec3f(1.0, 0.9, 0.7) * centerGlow; for (var s = 0; s < 20; s++) { let starAngle = f32(s) * 0.618 * 6.28318; let starR = 0.2 + f32(s) * 0.04; let starPos = vec2f(cos(starAngle + u.time * 0.1), sin(starAngle + u.time * 0.1)) * starR; let starDist = length(p - starPos); let freq = getFrequency(s * 6); let starGlow = exp(-starDist * 50.0) * (0.5 + freq); color += vec3f(1.0, 1.0, 1.0) * starGlow; } return color; } // DNA fn dnaVisualization(uv: vec2f, paletteId: i32) -> vec3f { var p = (uv - 0.5) * 2.0; p.x *= u.aspect; var color = vec3f(0.02, 0.02, 0.05); let scrollY = p.y + u.time * 0.5; for (var strand = 0; strand < 2; strand++) { let phase = f32(strand) * 3.14159; for (var i = 0; i < 20; i++) { let y = f32(i) * 0.15 - 1.5; let localY = scrollY + y; let x = sin(localY * 4.0 + phase) * 0.3; let freqIndex = i * 6; let freq = getFrequency(freqIndex); let nodeDist = length(p - vec2f(x, y)); let nodeGlow = exp(-nodeDist * 30.0); let hue = f32(i) / 20.0 + f32(strand) * 0.5; color += getPaletteColor(hue, paletteId) * nodeGlow * (0.5 + freq); } } for (var b = 0; b < 10; b++) { let y = f32(b) * 0.3 - 1.5 + fract(u.time * 0.5) * 0.3; let x1 = sin((scrollY + y) * 4.0) * 0.3; let x2 = sin((scrollY + y) * 4.0 + 3.14159) * 0.3; let freq = getFrequency(b * 12); if (p.y > y - 0.02 && p.y < y + 0.02 && p.x > min(x1, x2) && p.x < max(x1, x2)) { color += getPaletteColor(0.6, paletteId) * freq; } } return color; } // Fireworks fn fireworksVisualization(uv: vec2f, paletteId: i32) -> vec3f { var p = (uv - 0.5) * 2.0; p.x *= u.aspect; var color = vec3f(0.01, 0.01, 0.02); let bass = getBass(); for (var fw = 0; fw < 5; fw++) { let fwTime = u.time * 0.5 + f32(fw) * 1.2; let burstPhase = fract(fwTime); let fwX = sin(f32(fw) * 2.3 + u.time * 0.1) * 0.5; let fwY = cos(f32(fw) * 1.7) * 0.3; let fwPos = vec2f(fwX, fwY); let expandRadius = burstPhase * 0.8; let fade = 1.0 - burstPhase; for (var particle = 0; particle < 16; particle++) { let angle = f32(particle) / 16.0 * 6.28318; let freq = getFrequency(fw * 20 + particle * 2); let particleR = expandRadius * (0.8 + freq * 0.4); let particlePos = fwPos + vec2f(cos(angle), sin(angle)) * particleR; let particleDist = length(p - particlePos); let particleGlow = exp(-particleDist * 40.0) * fade * (0.5 + freq); let hue = f32(fw) / 5.0 + f32(particle) / 16.0 * 0.2; color += getPaletteColor(hue, paletteId) * particleGlow; let sparkTrail = exp(-particleDist * 20.0) * fade * 0.3 * freq; color += vec3f(1.0, 0.8, 0.5) * sparkTrail; } } color += getPaletteColor(0.5, paletteId) * bass * 0.3; return color; } // Pulsing Rings fn ringsVisualization(uv: vec2f, paletteId: i32) -> vec3f { var p = (uv - 0.5) * 2.0; p.x *= u.aspect; var color = vec3f(0.02, 0.02, 0.05); let r = length(p); for (var ring = 0; ring < 8; ring++) { let freqIndex = ring * 15; let freq = getFrequency(freqIndex); let baseRadius = 0.1 + f32(ring) * 0.12; let pulseRadius = baseRadius + freq * 0.1; let dist = abs(r - pulseRadius); let ringGlow = exp(-dist * 30.0) * (0.5 + freq); let hue = f32(ring) / 8.0 + u.time * 0.1; color += getPaletteColor(hue, paletteId) * ringGlow; } // Center pulse let bass = getBass(); let centerGlow = exp(-r * 8.0) * bass; color += getPaletteColor(0.0, paletteId) * centerGlow; return color; } // Sound Particles fn particlesVisualization(uv: vec2f, paletteId: i32) -> vec3f { var p = (uv - 0.5) * 2.0; p.x *= u.aspect; var color = vec3f(0.01, 0.01, 0.02); for (var i = 0; i < 32; i++) { let fi = f32(i); let freq = getFrequency(i * 4); let angle = fi * 0.618 * 6.28318 + u.time * 0.3; let radius = 0.2 + fi * 0.025 + freq * 0.2; let particlePos = vec2f(cos(angle), sin(angle)) * radius; let dist = length(p - particlePos); let size = 0.02 + freq * 0.03; let particleGlow = exp(-dist * 40.0 / size) * (0.3 + freq); let hue = fi / 32.0; color += getPaletteColor(hue, paletteId) * particleGlow; } // Trailing effect let bass = getBass(); let trail = exp(-length(p) * 3.0) * bass * 0.3; color += getPaletteColor(0.5, paletteId) * trail; return color; } @fragment fn fragmentMain(input: VertexOutput) -> @location(0) vec4f { let style = i32(u.style); let paletteId = i32(u.palette); var uv = input.uv; // Mirror effect if (u.doMirror > 0.5) { uv.x = abs(uv.x - 0.5) + 0.5; } var color: vec3f; if (style == 0) { color = barsVisualization(uv, paletteId); } else if (style == 1) { color = circularVisualization(uv, paletteId); } else if (style == 2) { color = waveformVisualization(uv, paletteId); } else if (style == 3) { color = spectrumVisualization(uv, paletteId); } else if (style == 4) { color = ivyVisualization(uv); } else if (style == 5) { color = galaxyVisualization(uv, paletteId); } else if (style == 6) { color = dnaVisualization(uv, paletteId); } else if (style == 7) { color = fireworksVisualization(uv, paletteId); } else if (style == 8) { color = ringsVisualization(uv, paletteId); } else { color = particlesVisualization(uv, paletteId); } // Glow effect if (u.doGlow > 0.5) { color = color * 1.2 + color * color * 0.3; } let vignette = 1.0 - length((input.uv - 0.5) * 1.5); color *= vignette; return vec4f(color, 1.0); } `; } } // Export window.AudioRenderer = AudioRenderer;