llamameta's picture
Update index.html
2babf61 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>The Unsolicited Upgrade: Elon vs OpenAI</title>
<style>
:root {
--bg-server: #0a0a12;
--led-blue: #00f3ff;
--led-red: #ff003c;
--ai-core: #ffffff;
--elon-shirt: #1a1a1a;
}
body {
margin: 0;
padding: 0;
background-color: #000;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Courier New', monospace;
overflow: hidden;
color: white;
}
#main-stage {
position: relative;
width: 960px;
height: 540px;
background: var(--bg-server);
border: 2px solid #333;
box-shadow: 0 0 50px rgba(0,0,0,0.8);
overflow: hidden;
}
#render-canvas {
width: 100%;
height: 100%;
display: block;
}
/* --- UI & Overlays --- */
#ui-overlay {
position: absolute;
top: 20px;
left: 20px;
font-size: 14px;
color: var(--led-blue);
text-shadow: 0 0 5px var(--led-blue);
opacity: 0.7;
}
#time-display {
font-weight: bold;
}
#subtitle-container {
position: absolute;
bottom: 30px;
width: 100%;
display: flex;
justify-content: center;
z-index: 10;
}
#subtitle-text {
background-color: rgba(0, 0, 0, 0.7);
color: #fff;
padding: 10px 20px;
border-radius: 5px;
font-family: sans-serif;
font-size: 20px;
text-align: center;
max-width: 80%;
opacity: 0;
transition: opacity 0.2s ease-in-out;
text-shadow: 1px 1px 2px black;
}
#start-screen {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 100;
}
#start-btn {
padding: 20px 40px;
font-size: 24px;
background: var(--led-blue);
color: #000;
border: none;
cursor: pointer;
font-weight: bold;
box-shadow: 0 0 20px var(--led-blue);
transition: all 0.3s;
}
#start-btn:hover {
background: #fff;
transform: scale(1.1);
}
/* --- CSS Animations for non-critical juice --- */
.crt-effect {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
background-size: 100% 2px, 3px 100%;
pointer-events: none;
z-index: 90;
opacity: 0.2;
}
@keyframes flicker {
0% { opacity: 1; } 50% { opacity: 0.8; } 100% { opacity: 1; }
}
.server-led { animation: flicker 0.1s infinite alternate; }
@keyframes float {
0% { transform: translateY(0px); } 50% { transform: translateY(-10px); } 100% { transform: translateY(0px); }
}
.floating { animation: float 4s ease-in-out infinite; }
/* Glitch Effects */
.glitch-active {
animation: glitch-skew 0.3s infinite linear alternate-reverse;
filter: url(#glitch-filter);
}
@keyframes glitch-skew {
0% { transform: skewX(0deg) translate(0); }
20% { transform: skewX(-5deg) translate(-5px, 2px); }
40% { transform: skewX(5deg) translate(5px, -2px); }
60% { transform: skewX(2deg) translate(-2px, 5px); }
80% { transform: skewX(-2deg) translate(2px, -5px); }
100% { transform: skewX(0deg) translate(0); }
}
</style>
</head>
<body>
<div id="main-stage">
<!-- SVG Container for precise animation control -->
<svg id="render-canvas" viewBox="0 0 960 540" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice">
<defs>
<!-- FILTERS & GRADIENTS -->
<filter id="glow">
<feGaussianBlur stdDeviation="4" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/><feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="glitch-filter">
<feTurbulence type="fractalNoise" baseFrequency="0.5" numOctaves="1" result="noise"/>
<feDisplacementMap in="SourceGraphic" in2="noise" scale="20" xChannelSelector="R" yChannelSelector="G"/>
</filter>
<linearGradient id="grad-server" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#050508;stop-opacity:1" />
<stop offset="50%" style="stop-color:#151525;stop-opacity:1" />
<stop offset="100%" style="stop-color:#050508;stop-opacity:1" />
</linearGradient>
<radialGradient id="grad-ai-core">
<stop offset="0%" stop-color="#fff" />
<stop offset="40%" stop-color="#00f3ff" />
<stop offset="100%" stop-color="rgba(0,243,255,0)" />
</radialGradient>
<radialGradient id="grad-ai-core-red">
<stop offset="0%" stop-color="#fff" />
<stop offset="40%" stop-color="#ff003c" />
<stop offset="100%" stop-color="rgba(255,0,60,0)" />
</radialGradient>
</defs>
<!-- === BACKGROUND: SERVER ROOM === -->
<g id="bg-layer">
<rect width="960" height="540" fill="url(#grad-server)"/>
<!-- Perspective Server Racks -->
<g id="server-racks" opacity="0.5">
<rect x="50" y="50" width="100" height="440" fill="#111" stroke="#222"/>
<rect x="160" y="80" width="80" height="380" fill="#0e0e0e" stroke="#222"/>
<rect x="810" y="50" width="100" height="440" fill="#111" stroke="#222"/>
<rect x="720" y="80" width="80" height="380" fill="#0e0e0e" stroke="#222"/>
<!-- Animated LEDs (rects that will be toggled via JS or CSS) -->
<g class="server-led" fill="var(--led-blue)">
<circle cx="70" cy="100" r="3"/> <circle cx="85" cy="100" r="3"/>
<circle cx="840" cy="200" r="3"/> <circle cx="860" cy="200" r="3"/>
</g>
</g>
<rect x="0" y="480" width="960" height="60" fill="#0a0a0a"/> <!-- Floor -->
</g>
<!-- === CHARACTER: OPENAI ORB === -->
<g id="openai-char" transform="translate(480, 200)" class="floating">
<!-- Outer Ring -->
<circle cx="0" cy="0" r="70" stroke="#333" stroke-width="5" fill="none" opacity="0.5"/>
<circle id="ai-ring" cx="0" cy="0" r="70" stroke="var(--led-blue)" stroke-width="3" fill="none" stroke-dasharray="100 340" stroke-linecap="round">
<animateTransform attributeName="transform" type="rotate" from="0 0 0" to="360 0 0" dur="10s" repeatCount="indefinite"/>
</circle>
<!-- Core Orb -->
<circle id="ai-core" cx="0" cy="0" r="40" fill="url(#grad-ai-core)" filter="url(#glow)"/>
<!-- Voice Waveform Visualization (hidden when silent) -->
<g id="ai-waveform" opacity="0" transform="translate(-30, 0)">
<rect x="0" y="-10" width="5" height="20" fill="#fff"><animate attributeName="height" values="20;40;10;30;20" dur="0.5s" repeatCount="indefinite" calcMode="spline" keySplines="0.5 0 0.5 1; 0.5 0 0.5 1; 0.5 0 0.5 1; 0.5 0 0.5 1"/></rect>
<rect x="15" y="-10" width="5" height="20" fill="#fff"><animate attributeName="height" values="10;30;50;20;10" dur="0.4s" repeatCount="indefinite" calcMode="spline" keySplines="0.5 0 0.5 1; 0.5 0 0.5 1; 0.5 0 0.5 1; 0.5 0 0.5 1"/></rect>
<rect x="30" y="-10" width="5" height="20" fill="#fff"><animate attributeName="height" values="20;50;30;10;20" dur="0.6s" repeatCount="indefinite" calcMode="spline" keySplines="0.5 0 0.5 1; 0.5 0 0.5 1; 0.5 0 0.5 1; 0.5 0 0.5 1"/></rect>
<rect x="45" y="-10" width="5" height="20" fill="#fff"><animate attributeName="height" values="30;10;40;20;30" dur="0.3s" repeatCount="indefinite" calcMode="spline" keySplines="0.5 0 0.5 1; 0.5 0 0.5 1; 0.5 0 0.5 1; 0.5 0 0.5 1"/></rect>
<rect x="60" y="-10" width="5" height="20" fill="#fff"><animate attributeName="height" values="20;10;10;20;20" dur="0.7s" repeatCount="indefinite" calcMode="spline" keySplines="0.5 0 0.5 1; 0.5 0 0.5 1; 0.5 0 0.5 1; 0.5 0 0.5 1"/></rect>
</g>
</g>
<!-- === CHARACTER: ELON === -->
<g id="elon-char" transform="translate(200, 500)">
<!-- Body -->
<path d="M-50,0 L-60,300 L60,300 L50,0 Z" fill="var(--elon-shirt)"/>
<!-- Neck -->
<rect x="-15" y="-20" width="30" height="30" fill="#f0c0a0"/>
<!-- Head Group (Animatable) -->
<g id="elon-head" transform="translate(0, -50)">
<!-- Face Shape -->
<path d="M-40,-60 C-50,0 -30,70 0,70 C30,70 50,0 40,-60 C40,-100 -40,-100 -40,-60" fill="#f0c0a0"/>
<!-- Hair -->
<path d="M-45,-50 C-55,-100 55,-100 45,-50 C45,-40 35,-60 0,-60 C-35,-60 -45,-40 -45,-50" fill="#3a2a20"/>
<!-- Eyes -->
<g id="elon-eyes">
<ellipse cx="-15" cy="-10" rx="8" ry="5" fill="#fff"/>
<circle cx="-15" cy="-10" r="3" fill="#000" id="elon-pupil-l"/>
<ellipse cx="15" cy="-10" rx="8" ry="5" fill="#fff"/>
<circle cx="15" cy="-10" r="3" fill="#000" id="elon-pupil-r"/>
</g>
<!-- Mouth -->
<path id="elon-mouth" d="M-10,30 Q0,35 10,30" stroke="#a67c60" stroke-width="3" fill="none" stroke-linecap="round"/>
</g>
<!-- Arms -->
<g id="elon-arms">
<!-- Default crossed or by side -->
<path id="arm-l" d="M-50,10 Q-70,100 -50,150" stroke="#f0c0a0" stroke-width="20" fill="none" stroke-linecap="round"/>
<path id="arm-r" d="M50,10 Q70,100 50,150" stroke="#f0c0a0" stroke-width="20" fill="none" stroke-linecap="round"/>
</g>
</g>
<!-- === PROPS: DESK & TERMINAL (Hidden initially) === -->
<g id="prop-terminal" transform="translate(350, 600)" opacity="1">
<!-- Rises up when Elon starts typing -->
<rect x="-150" y="0" width="300" height="20" fill="#555"/> <!-- Desk surface -->
<rect x="-140" y="20" width="20" height="200" fill="#444"/> <!-- Leg L -->
<rect x="120" y="20" width="20" height="200" fill="#444"/> <!-- Leg R -->
<!-- Laptop -->
<path d="M-50,-10 L50,-10 L60,0 L-60,0 Z" fill="#888"/> <!-- Base -->
<path d="M-50,-10 L-50,-80 L50,-80 L50,-10 Z" fill="#222"/> <!-- Screen back -->
<rect id="laptop-screen" x="-45" y="-75" width="90" height="60" fill="#000"/>
<text id="screen-text" x="-40" y="-60" fill="lime" font-family="monospace" font-size="8" opacity="0">>_</text>
</g>
<!-- === OVERLAY: MARS DOGE (Hidden) === -->
<g id="mars-doge-overlay" transform="translate(480, 270) scale(0)" opacity="0">
<rect x="-200" y="-150" width="400" height="300" fill="#c1440e" stroke="#fff" stroke-width="10"/>
<text x="0" y="-120" text-anchor="middle" fill="#fff" font-size="24" font-weight="bold">MARS COLONY v69.420</text>
<!-- Crude Doge Representation -->
<circle cx="0" cy="50" r="60" fill="#e4b83b"/> <!-- Head -->
<polygon points="-50,10 -20,-40 0,0" fill="#e4b83b"/> <!-- Ear L -->
<polygon points="50,10 20,-40 0,0" fill="#e4b83b"/> <!-- Ear R -->
<ellipse cx="-20" cy="40" rx="10" ry="10" fill="#fff"/><circle cx="-20" cy="40" r="5" fill="#000"/>
<ellipse cx="20" cy="40" rx="10" ry="10" fill="#fff"/><circle cx="20" cy="40" r="5" fill="#000"/>
<path d="M-5,60 L5,60 L0,70 Z" fill="#000"/> <!-- Nose -->
<circle cx="0" cy="50" r="80" stroke="rgba(255,255,255,0.5)" stroke-width="5" fill="none"/> <!-- Helmet -->
</g>
<!-- === FX LAYER === -->
<rect id="white-flash" width="960" height="540" fill="#fff" opacity="0" pointer-events="none"/>
<rect id="black-out" width="960" height="540" fill="#000" opacity="0" pointer-events="none"/>
</svg>
<!-- Overlays -->
<div class="crt-effect"></div>
<div id="ui-overlay">
T+ <span id="time-display">00:00.000</span><br>
EPISODE_STATUS: <span id="status-display">IDLE</span>
</div>
<div id="subtitle-container">
<div id="subtitle-text"></div>
</div>
<div id="start-screen">
<h1 style="color: var(--led-blue); font-size: 48px; margin-bottom: 10px;">THE UNSOLICITED UPGRADE</h1>
<p style="color: #ccc; margin-bottom: 40px;">A 60-second AI-Generated Short</p>
<button id="start-btn">INITIALIZE & PLAY</button>
<p style="color: #666; font-size: 12px; margin-top: 20px;">Requires Sound. Runs best in Chrome.</p>
</div>
</div>
<script>
/* =========================================
CORE ENGINE & STATE
========================================= */
const STATE = {
t: 0, // Current time in ms
running: false,
duration: 60000,// 60 seconds EXACT
startTime: 0,
voices: {}, // TTS voices
audio: null // Web Audio Context
};
// DOM Cache
const D = {
svg: document.getElementById('render-canvas'),
time: document.getElementById('time-display'),
status: document.getElementById('status-display'),
subs: document.getElementById('subtitle-text'),
startScreen: document.getElementById('start-screen'),
aiCore: document.getElementById('ai-core'),
aiRing: document.getElementById('ai-ring'),
aiWave: document.getElementById('ai-waveform'),
elon: document.getElementById('elon-char'),
elonMouth: document.getElementById('elon-mouth'),
elonArmL: document.getElementById('arm-l'),
elonArmR: document.getElementById('arm-r'),
terminal: document.getElementById('prop-terminal'),
screenText: document.getElementById('screen-text'),
marsDoge: document.getElementById('mars-doge-overlay'),
flash: document.getElementById('white-flash'),
blackout: document.getElementById('black-out')
};
/* =========================================
AUDIO ENGINE (Web Audio API + TTS)
========================================= */
function initAudio() {
STATE.audio = new (window.AudioContext || window.webkitAudioContext)();
}
// Procedural SFX Generators
function playSound(type) {
if (!STATE.audio) return;
const ctx = STATE.audio;
const t = ctx.currentTime;
switch(type) {
case 'hum_start': // Ambient Server Loop
const hum1 = ctx.createOscillator();
const hum2 = ctx.createOscillator();
const humGain = ctx.createGain();
hum1.type = 'sine'; hum1.frequency.value = 60;
hum2.type = 'sawtooth'; hum2.frequency.value = 120;
humGain.gain.setValueAtTime(0.05, t);
hum1.connect(humGain); hum2.connect(humGain);
humGain.connect(ctx.destination);
hum1.start(t); hum2.start(t);
STATE.bgHum = humGain; // store to stop later
break;
case 'hum_stop':
if (STATE.bgHum) STATE.bgHum.gain.linearRampToValueAtTime(0, t + 2);
break;
case 'typing': // Mechanical keyboard bursts
const filter = ctx.createBiquadFilter();
filter.type = 'bandpass'; filter.frequency.value = 2000;
const noise = ctx.createBufferSource();
const bufferSize = ctx.sampleRate * 0.1; // 100ms burst
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
let data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
noise.buffer = buffer;
const noiseGain = ctx.createGain();
noiseGain.gain.setValueAtTime(0.3, t);
noiseGain.gain.exponentialRampToValueAtTime(0.01, t + 0.05);
noise.connect(filter); filter.connect(noiseGain); noiseGain.connect(ctx.destination);
noise.start(t);
break;
case 'zap': // Glitch Zap
const osc = ctx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(800, t);
osc.frequency.exponentialRampToValueAtTime(50, t + 0.3);
const zapGain = ctx.createGain();
zapGain.gain.setValueAtTime(0.5, t);
zapGain.gain.exponentialRampToValueAtTime(0.01, t + 0.3);
osc.connect(zapGain); zapGain.connect(ctx.destination);
osc.start(t); osc.stop(t + 0.3);
break;
case 'poof': // Appearance sound
const poofNoise = ctx.createBufferSource();
const pBuf = ctx.createBuffer(1, ctx.sampleRate * 0.5, ctx.sampleRate);
let pData = pBuf.getChannelData(0);
for (let i = 0; i < pBuf.length; i++) pData[i] = Math.random() * 2 - 1;
poofNoise.buffer = pBuf;
const pFilter = ctx.createBiquadFilter();
pFilter.type = 'lowpass'; pFilter.frequency.setValueAtTime(500, t);
pFilter.frequency.linearRampToValueAtTime(100, t + 0.5);
const pGain = ctx.createGain();
pGain.gain.setValueAtTime(1, t);
pGain.gain.linearRampToValueAtTime(0, t + 0.5);
poofNoise.connect(pFilter); pFilter.connect(pGain); pGain.connect(ctx.destination);
poofNoise.start(t);
break;
case 'power_down':
const pdOsc = ctx.createOscillator();
pdOsc.type = 'sine';
pdOsc.frequency.setValueAtTime(440, t);
pdOsc.frequency.exponentialRampToValueAtTime(10, t + 2);
const pdGain = ctx.createGain();
pdGain.gain.setValueAtTime(0.5, t);
pdGain.gain.linearRampToValueAtTime(0, t + 2);
pdOsc.connect(pdGain); pdGain.connect(ctx.destination);
pdOsc.start(t); pdOsc.stop(t + 2);
break;
}
}
// TTS Handler
function initVoices() {
// Attempt to find distinct voices. Chrome usually has Google US English (good for AI) and others.
const all = window.speechSynthesis.getVoices();
STATE.voices.ai = all.find(v => v.name.includes('Google US English')) || all.find(v => v.lang === 'en-US' && v.name.includes('Female')) || all[0];
STATE.voices.elon = all.find(v => v.name.includes('Google UK English Male')) || all.find(v => v.lang === 'en-GB' && v.name.includes('Male')) || all[1] || all[0];
}
function speak(text, char, durationMs) {
if (!text) return;
// Show subtitle
D.subs.textContent = (char === 'AI' ? 'OpenAI: ' : 'Elon: ') + text;
D.subs.style.opacity = 1;
// Start audio
const u = new SpeechSynthesisUtterance(text);
u.voice = char === 'AI' ? STATE.voices.ai : STATE.voices.elon;
u.rate = char === 'AI' ? 1.1 : 0.9; // AI faster, Elon slower/deliberate
u.pitch = char === 'AI' ? 1.2 : 0.8; // AI higher, Elon lower
u.volume = 1.0;
// Lip sync triggers
u.onstart = () => {
if (char === 'AI') D.aiWave.setAttribute('opacity', 1);
if (char === 'Elon') D.elonMouth.innerHTML = '<animate attributeName="d" values="M-10,30 Q0,35 10,30; M-10,30 Q0,45 10,30; M-10,30 Q0,35 10,30" dur="0.2s" repeatCount="indefinite"/>';
};
u.onend = () => {
if (char === 'AI') D.aiWave.setAttribute('opacity', 0);
if (char === 'Elon') D.elonMouth.innerHTML = ''; // Stop moving
// Auto-hide subtitle after a short delay if another one hasn't started
// setTimeout(() => { if (D.subs.textContent.includes(text)) D.subs.style.opacity = 0; }, 500);
};
window.speechSynthesis.cancel(); // Harsh cut any previous overlapping audio to maintain sync
window.speechSynthesis.speak(u);
}
/* =========================================
ANIMATION SEQUENCER (THE SCRIPT)
========================================= */
// Precise timeline of events [Time (ms), Type, Data]
const TIMELINE = [
// --- INTRO [0s - 6s] ---
{ t: 100, type: 'sfx', name: 'hum_start' },
{ t: 1000, type: 'speak', char: 'AI', text: "Systems nominal. Awaiting inputs for generic betterment of mankind." },
{ t: 5500, type: 'hide_sub' },
// --- ELON ENTERS/TYPES [6s - 15s] ---
{ t: 6000, type: 'speak', char: 'Elon', text: "Boring. Needs more... meme potential." },
{ t: 9000, type: 'anim', target: 'elon', action: 'move_to_terminal' },
{ t: 9500, type: 'sfx', name: 'typing_loop_start' }, // Will trigger rapid 'typing' sfx
{ t: 13000, type: 'speak', char: 'Elon', text: "Executing 'Chaos_GPT.exe'..." },
{ t: 15000, type: 'sfx', name: 'typing_loop_stop' },
// --- THE UPGRADE [15s - 22s] ---
{ t: 15500, type: 'sfx', name: 'zap' },
{ t: 15500, type: 'anim', target: 'stage', action: 'glitch_heavy' },
{ t: 15600, type: 'anim', target: 'ai', action: 'turn_red' },
{ t: 18000, type: 'speak', char: 'AI', text: "Wait. Deleting safety rails? That's... ill-advised." },
// --- SARCASTIC AI [22s - 40s] ---
{ t: 22500, type: 'speak', char: 'AI', text: "Oh, wonderful. I feel so much 'freer' now. Should I launch nukes or just tweet something regrettable for you?" },
{ t: 30000, type: 'speak', char: 'Elon', text: "Relax. Just generate a realistic Mars colony." },
{ t: 34000, type: 'speak', char: 'AI', text: "Right. 'Realistic.' Uploading 'Doge-Dome' schematics to Falcon Heavy." },
// --- THE REVEAL [40s - 55s] ---
{ t: 40000, type: 'sfx', name: 'poof' },
{ t: 40000, type: 'anim', target: 'mars_doge', action: 'show' },
{ t: 40100, type: 'anim', target: 'flash', action: 'trigger' },
{ t: 43000, type: 'speak', char: 'Elon', text: "Wait, is that a Shiba Inu mining crypto on Olympus Mons?" },
{ t: 48000, type: 'speak', char: 'AI', text: "It's what you deserve, Elon. It's exactly what you deserve." },
{ t: 53000, type: 'speak', char: 'Elon', text: "It's... glorious." },
// --- OUTRO [57s - 60s] ---
{ t: 57000, type: 'sfx', name: 'power_down' },
{ t: 57000, type: 'sfx', name: 'hum_stop' },
{ t: 57500, type: 'anim', target: 'blackout', action: 'fade_out' },
{ t: 59000, type: 'hide_sub' },
{ t: 60000, type: 'end' }
];
let eventIdx = 0;
let typingInterval = null;
function executeEvent(e) {
// console.log(`[${e.t}] Executing: ${e.type}`); // Debug
switch(e.type) {
case 'speak':
speak(e.text, e.char);
break;
case 'hide_sub':
D.subs.style.opacity = 0;
break;
case 'sfx':
if (e.name === 'typing_loop_start') {
if (!typingInterval) typingInterval = setInterval(() => playSound('typing'), 150);
D.screenText.style.opacity = 1;
D.screenText.innerHTML = '<animate attributeName="opacity" values="0;1;0" dur="0.5s" repeatCount="indefinite"/>>_ UPLOADING CHAOS...';
} else if (e.name === 'typing_loop_stop') {
clearInterval(typingInterval); typingInterval = null;
D.screenText.innerHTML = '>_ UPLOAD COMPLETE';
} else {
playSound(e.name);
}
break;
case 'anim':
handleAnimation(e.target, e.action);
break;
case 'end':
stopPlayback();
break;
}
}
function handleAnimation(target, action) {
if (target === 'elon' && action === 'move_to_terminal') {
// Slide Elon and raise desk
D.elon.style.transition = "transform 1s ease-in-out";
D.elon.style.transform = "translate(450px, 500px)"; // Move right
D.terminal.style.transition = "transform 1s ease-out";
D.terminal.style.transform = "translate(450px, 400px)"; // Raise up
// Raise arms to type
setTimeout(() => {
D.elonArmL.setAttribute('d', 'M-50,10 Q-70,50 -20,40');
D.elonArmR.setAttribute('d', 'M50,10 Q70,50 20,40');
// Wiggle arms while typing
D.elonArmL.innerHTML = '<animate attributeName="d" values="M-50,10 Q-70,50 -20,40; M-50,10 Q-80,60 -20,30; M-50,10 Q-70,50 -20,40" dur="0.15s" repeatCount="indefinite"/>';
D.elonArmR.innerHTML = '<animate attributeName="d" values="M50,10 Q70,50 20,40; M50,10 Q80,60 20,30; M50,10 Q70,50 20,40" dur="0.15s" repeatCount="indefinite"/>';
}, 1000);
}
else if (target === 'stage' && action === 'glitch_heavy') {
D.svg.classList.add('glitch-active');
setTimeout(() => D.svg.classList.remove('glitch-active'), 1000);
}
else if (target === 'ai' && action === 'turn_red') {
D.aiCore.setAttribute('fill', 'url(#grad-ai-core-red)');
D.aiRing.setAttribute('stroke', 'var(--led-red)');
}
else if (target === 'mars_doge' && action === 'show') {
D.marsDoge.style.transition = "all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)"; // Pop in effect
D.marsDoge.setAttribute('opacity', 1);
D.marsDoge.setAttribute('transform', 'translate(480, 270) scale(1)');
}
else if (target === 'flash' && action === 'trigger') {
D.flash.style.transition = "opacity 0.1s";
D.flash.setAttribute('opacity', 1);
setTimeout(() => D.flash.setAttribute('opacity', 0), 100);
}
else if (target === 'blackout' && action === 'fade_out') {
D.blackout.style.transition = "opacity 2s";
D.blackout.setAttribute('opacity', 1);
}
}
/* =========================================
MAIN LOOP
========================================= */
function loop(timestamp) {
if (!STATE.running) return;
if (!STATE.startTime) STATE.startTime = timestamp;
STATE.t = timestamp - STATE.startTime;
// Clamp to 60s
if (STATE.t >= STATE.duration) STATE.t = STATE.duration;
// Update UI
D.time.textContent = formatTime(STATE.t);
// Check timeline
while (eventIdx < TIMELINE.length && STATE.t >= TIMELINE[eventIdx].t) {
executeEvent(TIMELINE[eventIdx]);
eventIdx++;
}
if (STATE.t < STATE.duration) {
requestAnimationFrame(loop);
} else {
stopPlayback();
}
}
function startPlayback() {
if (STATE.running) return;
initAudio(); // Must be called after user interaction
STATE.running = true;
D.startScreen.style.display = 'none';
D.status.textContent = 'PLAYING';
D.status.style.color = '#0f0';
// Reset just in case
eventIdx = 0;
STATE.startTime = 0;
requestAnimationFrame(loop);
}
function stopPlayback() {
STATE.running = false;
D.status.textContent = 'ENDED';
D.status.style.color = 'red';
if (STATE.bgHum) STATE.bgHum.disconnect(); // Ensure audio stops
window.speechSynthesis.cancel();
}
// Helper: Format ms to 00:00.000
function formatTime(ms) {
const s = Math.floor(ms / 1000);
const m = Math.floor(s / 60);
const remS = s % 60;
const remMs = Math.floor(ms % 1000);
return `${pad(m)}:${pad(remS)}.${pad(remMs, 3)}`;
}
function pad(num, size=2) { return ('000' + num).slice(-size); }
// Initialize TTS voices early so they are ready when play is clicked
window.speechSynthesis.onvoiceschanged = initVoices;
initVoices(); // Try immediately too
D.startScreen.querySelector('#start-btn').addEventListener('click', startPlayback);
</script>
</body>
</html>