This is a submission for Weekend Challenge: Passion Edition
What I Built
Fandom Fire is a real-time, procedural AI roast battle engine. You pick any two fictional characters — Superman vs Thor, Batman vs Joker, Goku vs Naruto — and the engine writes original roasts, synthesizes distinct character voices, and projects a gorgeous audio-reactive fluid visualizer directly onto your GPU.
The entire experience is driven by three AI systems working in concert:
-
Google Gemini (
gemini-3.1-flash-lite) writes the roasts procedurally — no scripts, no templates. Every battle is unique. -
ElevenLabs (
eleven_v3) gives each fighter a distinct, high-fidelity voice. Fighter A gets a deep, aggressive tone. Fighter B gets a sharp, theatrical British delivery. - A Custom GLSL Fragment Shader renders a Gemini Live-inspired fluid wave that physically reacts to the bass frequencies of each voice in real-time.
There is no backend. No npm. No build step. Just three files (index.html, style.css, app.js), a Python static server, and raw browser APIs.
Demo
Code
Kaushikcoderpy
/
Fandom-Fire
Fandom Fire is a real-time, procedural AI roast battle engine. You pick any two fictional characters — Superman vs Thor, Batman vs Joker, Goku vs Naruto — and the engine writes original roasts, synthesizes distinct character voices, and projects a gorgeous audio-reactive fluid visualizer directly onto your GPU.
🔥 Fandom Fire
Fandom Fire is a sensory-heavy, hardware-accelerated, procedural AI-generated roast battle engine. It takes any two fictional characters, generates custom roasts on the fly using Google Gemini, synthesizes their distinct voices using ElevenLabs, and projects a gorgeous, audio-reactive fluid visualizer inspired directly by Gemini Live.
Built entirely on a pure frontend stack—no backend, no npm installs, and no local build systems required.
🚀 Key Visual & Architectural Highlights
1. 🌌 Gemini Live Fluid Wave (WebGL + GLSL)
- We replaced standard, boring 2D cards and orbiting wireframes with a full-viewport screen-space custom GLSL Fragment Shader running directly on the GPU.
- Blends organic, eye-comforting color gradients
-
Sky Blue (
#022aff) representing the Left Fighter. -
Premium Indigo/Violet (
#7a1ef0) blending in the center. -
Rose Red/Pink (
#f50d6b) representing the Right Fighter.
-
Sky Blue (
- An organic mathematical wave deforms at the bottom of the screen using multi-octave simplex/value noise…
How I Built It
The Architecture (3 Layers)
The engine operates as a pipeline with three distinct layers:
[User Input] → [Gemini Roast Generator] → [ElevenLabs Voice Synth] → [Web Audio FFT] → [GLSL Shader]
Layer 1 generates the text. Layer 2 gives it a voice. Layer 3 makes the voice visible.
Layer 1: Gemini Roast Generation (Best Use of Google AI)
When you click "ENGAGE BATTLE ENGINE," the app fires off alternating prompts to gemini-3.1-flash-lite:
async function generateBattle(f1, f2) {
const turns = [
{ attacker: f1, defender: f2, side: 'left' },
{ attacker: f2, defender: f1, side: 'right' },
{ attacker: f1, defender: f2, side: 'left' },
{ attacker: f2, defender: f1, side: 'right' }
];
for(let i=0; i<turns.length; i++) {
if(!isBattling) break;
const turn = turns[i];
const prompt = `You are ${turn.attacker}. Write a single, highly
aggressive, funny, and punchy 1-2 sentence roast insulting
${turn.defender}. Use VERY SIMPLE, EASY TO UNDERSTAND language.
Do not include quotes, hashtags, or emojis. Just the raw dialogue.
Speak in: ${preferredLang}.`;
const response = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-flash-lite:generateContent?key=${geminiKey}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
systemInstruction: {
parts: [{ text: "You are a roast character. Output ONLY the raw roast text. No formatting." }]
}
})
});
const data = await response.json();
const roastText = data.candidates[0].content.parts[0].text
.replace(/["\n]/g, '').trim();
roastQueue.push({
attacker: turn.attacker,
side: turn.side,
text: roastText,
dmg: Math.floor(Math.random() * 21) + 40,
voice: turn.side === 'left' ? VOICE_LEFT : VOICE_RIGHT
});
}
}
Key design decision: We use gemini-3.1-flash-lite specifically because latency matters more than depth here. The roasts need to land fast. The systemInstruction enforces raw text output — no markdown, no emoji, no quotes — so the pipeline never chokes on formatting artifacts.
Layer 2: ElevenLabs Voice Synthesis (Best Use of ElevenLabs)
Each generated roast is immediately piped to ElevenLabs' eleven_v3 model with character-specific voice IDs:
function playTurn(turn) {
return new Promise(async (resolve) => {
// Fetch synthesized audio from ElevenLabs
const response = await fetch(
`https://api.elevenlabs.io/v1/text-to-speech/${turn.voice}?output_format=mp3_44100_128`,
{
method: 'POST',
headers: { 'xi-api-key': elevenKey, 'Content-Type': 'application/json' },
body: JSON.stringify({
text: turn.text,
model_id: "eleven_v3",
voice_settings: { stability: 0.5, similarity_boost: 0.75 }
})
});
// Decode directly into Web Audio API buffer
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
// Route through analyser node for FFT data extraction
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(analyser); // ← FFT tap point
analyser.connect(audioContext.destination);
source.start(0);
});
}
The critical routing decision: instead of playing the audio directly to audioContext.destination, we insert an AnalyserNode in between. This gives us access to real-time frequency data (getByteFrequencyData) without affecting playback quality. This is the bridge between Layer 2 and Layer 3.
Layer 3: The GLSL Fluid Wave (The Visual Heartbeat)
This is where the magic lives. A full-viewport PlaneGeometry is rendered via Three.js with a custom ShaderMaterial. The fragment shader generates an organic, flowing liquid wave inspired by Google's Gemini Live interface:
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
// Multi-octave noise generates organic wave forms
float n1 = noise(vec2(uv.x * 2.5 + u_time * 0.6, u_time * 0.4));
float n2 = noise(vec2(uv.x * 4.5 - u_time * 0.4, u_time * 0.3));
// Audio volumes directly modulate wave height
float leftHeight = u_leftVolume * 0.28;
float rightHeight = u_rightVolume * 0.28;
float waveHeight = 0.15
+ n1 * (0.12 + leftHeight)
+ n2 * (0.08 + rightHeight);
// Smooth, glowing boundary (no harsh edges)
float alpha = smoothstep(-0.35, 0.05, waveHeight - uv.y);
// Premium gradient: Sky Blue → Violet → Rose Pink
vec3 colorLeft = vec3(0.02, 0.42, 0.98);
vec3 colorMid = vec3(0.48, 0.12, 0.94);
vec3 colorRight = vec3(0.96, 0.05, 0.42);
vec3 gradColor = mix(colorLeft, colorMid, smoothstep(0.1, 0.5, uv.x));
gradColor = mix(gradColor, colorRight, smoothstep(0.5, 0.9, uv.x));
// Brightness surges with voice volume
float intensity = 1.0 + (u_leftVolume + u_rightVolume) * 1.5;
vec3 bgColor = vec3(0.004, 0.004, 0.008) * (1.0 - uv.y);
gl_FragColor = vec4(mix(bgColor, gradColor * intensity, alpha), 1.0);
}
And the JavaScript render loop that feeds it live audio data every frame:
function animate() {
requestAnimationFrame(animate);
uniforms.u_time.value = clock.getElapsedTime();
if(analyser && isBattling) {
const dataArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(dataArray);
// Extract bass frequencies (bins 0-19) for organic pulsing
let sum = 0;
for(let i = 0; i < 20; i++) sum += dataArray[i];
let normalized = (sum / 20.0) / 255.0;
// Lerp prevents jarring visual jumps between frames
if (currentSpeakerSide === 'left') {
uniforms.u_leftVolume.value = THREE.MathUtils.lerp(
uniforms.u_leftVolume.value, normalized, 0.2);
uniforms.u_rightVolume.value = THREE.MathUtils.lerp(
uniforms.u_rightVolume.value, 0, 0.1);
} else if (currentSpeakerSide === 'right') {
uniforms.u_rightVolume.value = THREE.MathUtils.lerp(
uniforms.u_rightVolume.value, normalized, 0.2);
uniforms.u_leftVolume.value = THREE.MathUtils.lerp(
uniforms.u_leftVolume.value, 0, 0.1);
}
}
renderer.render(scene, camera);
}
The lerp (linear interpolation) on the volume values is essential — without it, the wave would jitter violently between frames. The 0.2 attack / 0.1 decay rates create a smooth, organic breathing effect that feels alive.
The UI: Tactile Brutalism
The visual language is deliberately anti-glassmorphism:
- Zero border-radius anywhere in the entire app
- Sharp 1px solid white borders on pitch black
- SVG-based fractal noise overlay for film grain texture
- Chromatic aberration text animation on roast impact
- Health bars with inline damage ticks (
-45DMG) that appear post-speech
The Playback Queue Architecture
The entire battle runs on a producer-consumer queue pattern:
generateBattle() → pushes roasts to roastQueue[]
playbackWorker() → shifts from roastQueue[], plays each turn sequentially
generateBattle() runs ahead, pre-fetching roasts from Gemini while the current turn is still playing. This hides API latency behind the ElevenLabs audio playback window. The user never waits.
Prize Categories
-
Best Use of Google AI — Gemini
gemini-3.1-flash-litegenerates all roast dialogue procedurally with zero templates. -
Best Use of ElevenLabs —
eleven_v3synthesizes distinct character voices routed through Web Audio for real-time FFT-driven shader visuals.
Zero-Dependency Stack
| Layer | Technology |
|---|---|
| Rendering | Three.js r128 (CDN) |
| Animation | GSAP 3.12.2 (CDN) |
| AI Text | Google Gemini gemini-3.1-flash-lite
|
| AI Voice | ElevenLabs eleven_v3
|
| Audio Analysis | Native Web Audio API |
| GPU Shaders | Custom GLSL Fragment Shader |
| Build System | None. python -m http.server 8000
|


Top comments (1)
This is a fascinating concept! Did you use something like