Spaces:
Running
Running
| /** | |
| * 🌿 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<uniform> u: Uniforms; | |
| @group(0) @binding(1) var<storage, read> audioData: array<f32, 128>; | |
| @group(0) @binding(2) var<storage, read> waveform: array<f32, 256>; | |
| struct VertexOutput { | |
| @builtin(position) position: vec4f, | |
| @location(0) uv: vec2f, | |
| } | |
| @vertex | |
| fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { | |
| var pos = array<vec2f, 3>( | |
| 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; | |