BACK TO EXPERIMENTS

CONTROLS

Audio Waves

CSS
<style>
        body .experiment-container { margin: 0; overflow: hidden; background-color: #0c0c0f; display: flex; justify-content: center; align-items: center; min-height: 100vh; font-family: monospace; }
        #audioWaveCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1; }
        #youtube-container { position: relative; z-index: 2; width: 80vw; max-width: 800px; aspect-ratio: 16 / 9; background-color: #000; border-radius: 8px; overflow: hidden; transition: box-shadow 0.3s ease; }
        #player { width: 100%; height: 100%; }
        #controls-dialog { position: absolute; top: 20px; right: 20px; background-color: rgba(30, 30, 30, 0.9); border-radius: 8px; padding: 15px; box-shadow: 0 0 15px rgba(0, 0, 0, 0.5); z-index: 10; max-height: calc(100vh - 40px); overflow-y: auto; color: #eee; width: 250px; border: 1px solid rgba(255, 255, 255, 0.1); }
        #controls-dialog h3 { margin-top: 0; text-align: center; font-weight: 300; letter-spacing: 1px; }
        .control-group { margin-bottom: 12px; }
        .control-group label { display: block; margin-bottom: 5px; font-size: 0.9em; color: #ccc; }
        .control-group input[type="range"] { width: 100%; }
        .control-group span { font-size: 0.8em; float: right; color: #aaa; }
        #manual-toggle-button {
            position: absolute;
            bottom: 20px;
            left: 20px;
            z-index: 10;
            padding: 10px 15px;
            font-family: monospace;
            background-color: #00ffff;
            color: #0c0c0f;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            box-shadow: 0 0 10px #00ffff;
        }
    </style>
JS
<script>
        const canvas = document.getElementById('audioWaveCanvas');
        const ctx = canvas.getContext('2d');
        const youtubeContainer = document.getElementById('youtube-container');
        let width, height;

        const dataPoints = 128;
        let audioData = new Float32Array(dataPoints).fill(0);
        let targetAudioData = new Float32Array(dataPoints).fill(0);

        let player;
        let isApiReady = false;
        let playerState = -1;
        let isManualToggleActive = false;
        let mouseX = -1000, mouseY = -1000;
        const INTERACTION_RADIUS = 200;

        let lastIdleUpdateTime = 0;
        let lastActiveUpdateTime = 0;
        const particles = [];
        const MAX_PARTICLES = 3000;
        let particleEmissionTimer = 0;
        const PARTICLE_EMISSION_RATE = 2;

        class Particle {
            constructor(x, y, controls) {
                this.x = x; this.y = y;
                const speedMultiplier = 0.2 + Math.random() * 0.8;
                this.vx = (Math.random() - 0.5) * 3 * speedMultiplier;
                this.vy = (Math.random() - 0.5) * 3 * speedMultiplier;
                this.life = 0;
                this.maxLife = controls.particleLifespan * (0.5 + Math.random());
                this.size = 0.5 + Math.random() * 2.5;
                this.hue = controls.colorHue + (Math.random() - 0.5) * 20;
            }
            update() { this.x += this.vx; this.y += this.vy; this.life++; return this.life < this.maxLife; }
            draw() {
                const alpha = 1 - (this.life / this.maxLife);
                ctx.fillStyle = `hsla(${this.hue}, 100%, 80%, ${alpha})`;
                ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fill();
            }
        }

        const controls = {
            idleAmplitude: 1, activeAmplitude: 7.5, pulseSpeed: 50, activeSpeed: 150, waveFlow: 0.005, cursorPush: 0.5,
            lineCount: 100, fraying: 6.9, colorHue: 180, particleSpeed: 2,
            particleLifespan: 190, particleBurst: 3,
        };

        function setupControls() {
            Object.keys(controls).forEach(key => {
                const slider = document.getElementById(`${key}-slider`);
                const valueSpan = document.getElementById(`${key}-val`);
                if (slider) {
                    slider.oninput = (e) => {
                        controls[key] = parseFloat(e.target.value);
                        if (valueSpan) valueSpan.textContent = controls[key];
                        if (key === 'colorHue') updateYoutubeGlow();
                    };
                }
            });
            updateYoutubeGlow();
        }

        function updateYoutubeGlow() {
            youtubeContainer.style.boxShadow = `0 0 35px hsla(${controls.colorHue}, 90%, 50%, 0.6)`;
        }

        function resizeCanvas() { width = canvas.width = window.innerWidth; height = canvas.height = window.innerHeight; }
        window.addEventListener('resize', resizeCanvas); resizeCanvas();
        window.addEventListener('mousemove', (e) => { mouseX = e.clientX; mouseY = e.clientY; });
        window.addEventListener('mouseleave', () => { mouseX = -1000; mouseY = -1000; });

        function onYouTubeIframeAPIReady() {
            isApiReady = true;
            player = new YT.Player('player', {
                height: '100%', width: '100%', videoId: 'ughJpX003Vw',
                playerVars: { 'playsinline': 1, 'origin': window.location.origin, 'controls': 1, 'rel': 0, 'enablejsapi': 1 },
                events: { 'onStateChange': onPlayerStateChange }
            });
        }
        function onPlayerStateChange(event) { playerState = event.data; }
        
        function drawDebugInfo(engineName) {
            ctx.font = "16px monospace"; ctx.shadowColor = "#000"; ctx.shadowBlur = 5;
            let statusText, statusColor;
            if (isApiReady && playerState === YT.PlayerState.PLAYING) { statusText = "YT STATE: PLAYING"; statusColor = "#00ff00"; }
            else { statusText = `YT STATE: INACTIVE (${playerState})`; statusColor = "#cccccc"; }
            ctx.fillStyle = statusColor; ctx.fillText(statusText, 15, 30);
            ctx.fillStyle = "#00ffff"; ctx.fillText(`ENGINE: ${engineName}`, 15, 50);
            ctx.fillStyle = "#ffff00"; ctx.fillText(`Particles: ${particles.length}`, 15, 70);
            ctx.shadowBlur = 0;
        }

        function runIdleEngine(timestamp) {
            if (timestamp - lastIdleUpdateTime > controls.pulseSpeed) {
                const amp = 10 * controls.idleAmplitude;
                for (let i = 0; i < dataPoints; i++) { targetAudioData[i] = (Math.random() - 0.5) * amp; }
                lastIdleUpdateTime = timestamp;
            }
            for (let i = 0; i < dataPoints; i++) { audioData[i] += (targetAudioData[i] - audioData[i]) * 0.1; }
        }

        function runActiveEngine(timestamp) {
            if (timestamp - lastActiveUpdateTime > controls.activeSpeed) {
                const amp = 15 * controls.activeAmplitude;
                for (let i = 0; i < dataPoints; i++) {
                    const slowSine = Math.sin(i * 0.1 + timestamp * controls.waveFlow);
                    const mediumSine = Math.sin(i * 0.4 + timestamp * controls.waveFlow * 2);
                    const fastSine = Math.sin(i * 0.9 + timestamp * controls.waveFlow * 4);
                    targetAudioData[i] = (slowSine * 0.5 + mediumSine * 0.3 + fastSine * 0.2) * amp;
                }
                lastActiveUpdateTime = timestamp;
            }
            for (let i = 0; i < dataPoints; i++) {
                audioData[i] += (targetAudioData[i] - audioData[i]) * 0.1;
            }
        }

        function animate(timestamp) {
            requestAnimationFrame(animate);
            ctx.clearRect(0, 0, width, height);

            let currentEngine = "IDLE ENGINE";
            const isActive = (isApiReady && playerState === YT.PlayerState.PLAYING) || isManualToggleActive;

            if (isActive) {
                currentEngine = "ACTIVE ENGINE";
                runActiveEngine(timestamp);
            } else {
                runIdleEngine(timestamp);
            }

            const waveCenterY = height / 2;
            particleEmissionTimer--;

            for (let lineIndex = 0; lineIndex < controls.lineCount; lineIndex++) {
                ctx.beginPath();
                const centerDist = Math.abs(lineIndex - controls.lineCount / 2);
                const maxDist = controls.lineCount / 2;
                const brightness = 1 - (centerDist / maxDist) * 0.6;
                const opacity = 1.0 - (centerDist / maxDist) * 0.8;
                ctx.strokeStyle = `hsla(${controls.colorHue}, 90%, ${brightness * 100}%, ${opacity})`;
                ctx.lineWidth = 2.5 - (centerDist / maxDist) * 2.0;
                ctx.lineJoin = 'miter';

                for (let i = 0; i < dataPoints; i++) {
                    const x = (i / (dataPoints - 1)) * width;
                    let y = waveCenterY + audioData[i] * (1 - (centerDist / maxDist) * 0.5);
                    const frayAmount = centerDist * controls.fraying * (audioData[i] / 50) * (Math.random() - 0.5);
                    y += frayAmount;

                    const dx = x - mouseX; const dy = y - mouseY;
                    const distToMouse = Math.sqrt(dx * dx + dy * dy);
                    if (distToMouse < INTERACTION_RADIUS) {
                        const force = 1 - distToMouse / INTERACTION_RADIUS;
                        const displacement = force * 150 * controls.cursorPush;
                        y += (dy / (distToMouse || 1)) * displacement;
                    }
                    if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
                }
                ctx.stroke();
            }
            
            if (isActive && particleEmissionTimer <= 0 && particles.length < MAX_PARTICLES) {
                const ampThreshold = (15 * controls.activeAmplitude) * 0.5;
                for (let i = 0; i < dataPoints; i++) {
                    if (Math.abs(audioData[i]) > ampThreshold) {
                        for(let p = 0; p < controls.particleBurst; p++) {
                            const x = (i / (dataPoints - 1)) * width;
                            const y = waveCenterY + audioData[i];
                            particles.push(new Particle(x, y, controls));
                        }
                    }
                }
                particleEmissionTimer = PARTICLE_EMISSION_RATE;
            }
            
            ctx.shadowBlur = 0;
            for (let i = particles.length - 1; i >= 0; i--) {
                if (particles[i].update()) particles[i].draw(); else particles.splice(i, 1);
            }
            drawDebugInfo(currentEngine);
        }

        const toggleButton = document.getElementById('manual-toggle-button');
        toggleButton.addEventListener('click', () => {
            isManualToggleActive = !isManualToggleActive;
            toggleButton.textContent = isManualToggleActive ? 'Deactivate Wave' : 'Activate Wave';
        });

        var tag = document.createElement('script');
        tag.src = "https://www.youtube.com/iframe_api";
        var firstScriptTag = document.getElementsByTagName('script')[0];
        firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

        setupControls();
        animate(0);
    </script>