Elysia-Suite's picture
Upload 23 files
e5d943e verified
/**
* 🌿 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;