// GrooveToon Interactive - Main Script // Global State const state = { isPlaying: false, currentDance: 'bounce', tempo: 120, accentColor: 'primary', secondaryColor: 'secondary', audioContext: null, beatInterval: null, particles: [], characterMood: 'happy' }; // Color Themes const themes = { primary: { 500: '#f43f5e', 600: '#e11d48', 400: '#fb7185' }, secondary: { 500: '#8b5cf6', 600: '#7c3aed', 400: '#a78bfa' } }; // Dance Moves Library const danceMoves = { bounce: { name: 'Bounce', css: { body: 'animation: dance-bounce 0.5s ease-in-out infinite', arms: 'animation: dance-wave 1s ease-in-out infinite', head: 'animation: dance-head-bop 0.5s ease-in-out infinite' } }, sway: { name: 'Sway', css: { body: 'animation: dance-sway 2s ease-in-out infinite', arms: 'animation: dance-wave 2s ease-in-out infinite reverse', head: 'animation: dance-sway 2s ease-in-out infinite 0.5s' } }, spin: { name: 'Spin', css: { body: 'animation: dance-spin 2s ease-in-out infinite', arms: 'animation: dance-sway 1s ease-in-out infinite', head: 'animation: none' } }, wave: { name: 'Wave', css: { body: 'animation: float-gentle 3s ease-in-out infinite', arms: 'animation: dance-wave 0.5s ease-in-out infinite', head: 'animation: dance-head-bop 0.25s ease-in-out infinite' } }, idle: { name: 'Idle', css: { body: 'animation: float-gentle 4s ease-in-out infinite', arms: 'animation: none', head: 'animation: none' } } }; // Audio System class AudioEngine { constructor() { this.ctx = null; this.masterGain = null; this.isInitialized = false; } init() { if (this.isInitialized) return; this.ctx = new (window.AudioContext || window.webkitAudioContext)(); this.masterGain = this.ctx.createGain(); this.masterGain.gain.value = 0.3; this.masterGain.connect(this.ctx.destination); this.isInitialized = true; } playKick(time) { const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.frequency.setValueAtTime(150, time); osc.frequency.exponentialRampToValueAtTime(0.01, time + 0.5); gain.gain.setValueAtTime(1, time); gain.gain.exponentialRampToValueAtTime(0.01, time + 0.5); osc.connect(gain); gain.connect(this.masterGain); osc.start(time); osc.stop(time + 0.5); } playHiHat(time) { const bufferSize = this.ctx.sampleRate * 0.1; const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) { data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / bufferSize, 2); } const noise = this.ctx.createBufferSource(); noise.buffer = buffer; const filter = this.ctx.createBiquadFilter(); filter.type = 'highpass'; filter.frequency.value = 5000; const gain = this.ctx.createGain(); gain.gain.value = 0.3; noise.connect(filter); filter.connect(gain); gain.connect(this.masterGain); noise.start(time); } playSnare(time) { const bufferSize = this.ctx.sampleRate * 0.2; const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) { data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (this.ctx.sampleRate * 0.05)); } const noise = this.ctx.createBufferSource(); noise.buffer = buffer; const filter = this.ctx.createBiquadFilter(); filter.type = 'highpass'; filter.frequency.value = 1000; const gain = this.ctx.createGain(); gain.gain.setValueAtTime(0.5, time); gain.gain.exponentialRampToValueAtTime(0.01, time + 0.2); noise.connect(filter); filter.connect(gain); gain.connect(this.masterGain); noise.start(time); } setTempo(bpm) { state.tempo = bpm; if (state.isPlaying) { this.stopBeat(); this.startBeat(); } } startBeat() { this.init(); const beatDuration = 60 / state.tempo; let beatCount = 0; const scheduleBeat = () => { const now = this.ctx.currentTime; const nextBeat = Math.ceil(now / beatDuration) * beatDuration; this.playKick(nextBeat); if (beatCount % 2 === 1) this.playSnare(nextBeat); this.playHiHat(nextBeat + beatDuration / 2); beatCount++; const delay = (nextBeat - now) * 1000; this.beatTimeout = setTimeout(scheduleBeat, delay); }; scheduleBeat(); // Visual beat indicator state.beatInterval = setInterval(() => { document.dispatchEvent(new CustomEvent('beat')); }, beatDuration * 1000); } stopBeat() { clearTimeout(this.beatTimeout); clearInterval(state.beatInterval); } } // Visual Effects Engine class VisualEffects { constructor() { this.canvas = document.getElementById('bgCanvas'); this.ctx = this.canvas.getContext('2d'); this.particles = []; this.resize(); window.addEventListener('resize', () => this.resize()); this.animate(); } resize() { this.canvas.width = window.innerWidth; this.canvas.height = window.innerHeight; } createParticle(x, y, type = 'sparkle') { const particle = { x, y, vx: (Math.random() - 0.5) * 4, vy: -Math.random() * 3 - 1, life: 1, decay: 0.02, size: Math.random() * 4 + 2, hue: Math.random() > 0.5 ? 348 : 262, // primary or secondary hue type }; this.particles.push(particle); } createBurst(x, y) { for (let i = 0; i < 20; i++) { this.createParticle(x, y, 'burst'); } } animate() { this.ctx.fillStyle = 'rgba(10, 10, 10, 0.1)'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); // Draw connecting lines between nearby particles for (let i = 0; i < this.particles.length; i++) { for (let j = i + 1; j < this.particles.length; j++) { const dx = this.particles[i].x - this.particles[j].x; const dy = this.particles[i].y - this.particles[j].y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 100) { this.ctx.beginPath(); this.ctx.strokeStyle = `hsla(${this.particles[i].hue}, 70%, 60%, ${0.1 * (1 - dist/100)})`; this.ctx.lineWidth = 1; this.ctx.moveTo(this.particles[i].x, this.particles[i].y); this.ctx.lineTo(this.particles[j].x, this.particles[j].y); this.ctx.stroke(); } } } // Update and draw particles this.particles = this.particles.filter(p => { p.x += p.vx; p.y += p.vy; p.vy += 0.05; // gravity p.life -= p.decay; if (p.life > 0) { this.ctx.beginPath(); this.ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2); this.ctx.fillStyle = `hsla(${p.hue}, 80%, 60%, ${p.life})`; this.ctx.fill(); return true; } return false; }); // Draw subtle grid this.drawGrid(); requestAnimationFrame(() => this.animate()); } drawGrid() { const time = Date.now() * 0.001; const gridSize = 50; this.ctx.strokeStyle = 'rgba(244, 63, 94, 0.03)'; this.ctx.lineWidth = 1; for (let x = 0; x < this.canvas.width; x += gridSize) { const wave = Math.sin(x * 0.01 + time) * 10; this.ctx.beginPath(); this.ctx.moveTo(x, 0); this.ctx.lineTo(x + wave, this.canvas.height); this.ctx.stroke(); } for (let y = 0; y < this.canvas.height; y += gridSize) { const wave = Math.cos(y * 0.01 + time) * 10; this.ctx.beginPath(); this.ctx.moveTo(0, y); this.ctx.lineTo(this.canvas.width, y + wave); this.ctx.stroke(); } } } // Character Controller class CharacterController { constructor() { this.element = document.getElementById('dancer'); this.currentMove = 'idle'; } setDance(moveName) { this.currentMove = moveName; const move = danceMoves[moveName] || danceMoves.idle; // Apply CSS animations to character parts const characterParts = this.element.shadowRoot; if (!characterParts) return; const body = characterParts.getElementById('char-body'); const leftArm = characterParts.getElementById('char-arm-left'); const rightArm = characterParts.getElementById('char-arm-right'); const head = characterParts.getElementById('char-head'); if (body) body.style.cssText = move.css.body; if (leftArm) leftArm.style.cssText = move.css.arms; if (rightArm) rightArm.style.cssText = move.css.arms + ';animation-direction: reverse;'; if (head) head.style.cssText = move.css.head; // Dispatch event for UI update document.dispatchEvent(new CustomEvent('dance-change', { detail: moveName })); } setMood(mood) { state.characterMood = mood; const characterParts = this.element.shadowRoot; if (!characterParts) return; const face = characterParts.getElementById('char-face'); if (face) { face.setAttribute('data-mood', mood); } } pulse() { const body = this.element.shadowRoot?.getElementById('char-body'); if (body) { body.style.filter = 'brightness(1.3)'; setTimeout(() => { body.style.filter = 'brightness(1)'; }, 100); } } } // Main App Controller class GrooveApp { constructor() { this.audio = new AudioEngine(); this.visuals = new VisualEffects(); this.character = new CharacterController(); this.initListeners(); this.character.setDance('idle'); } initListeners() { // Play/Pause document.addEventListener('toggle-play', (e) => { state.isPlaying = e.detail.playing; if (state.isPlaying) { this.audio.startBeat(); this.character.setDance(state.currentDance); } else { this.audio.stopBeat(); this.character.setDance('idle'); } }); // Dance change document.addEventListener('dance-select', (e) => { state.currentDance = e.detail.dance; if (state.isPlaying) { this.character.setDance(state.currentDance); } }); // Tempo change document.addEventListener('tempo-change', (e) => { this.audio.setTempo(e.detail.tempo); }); // Beat event document.addEventListener('beat', () => { if (state.isPlaying) { this.character.pulse(); // Random particles const x = Math.random() * window.innerWidth; const y = window.innerHeight - 200; this.visuals.createParticle(x, y); } }); // Color change document.addEventListener('color-change', (e) => { const { primary, secondary } = e.detail; document.documentElement.style.setProperty('--primary-hue', this.hexToHue(primary)); document.documentElement.style.setProperty('--secondary-hue', this.hexToHue(secondary)); }); // Click effects document.addEventListener('click', (e) => { if (e.target.closest('button') || e.target.closest('.control-btn')) { this.visuals.createBurst(e.clientX, e.clientY); } }); } hexToHue(hex) { const r = parseInt(hex.slice(1, 3), 16) / 255; const g = parseInt(hex.slice(3, 5), 16) / 255; const b = parseInt(hex.slice(5, 7), 16) / 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); let h = 0; if (max !== min) { const d = max - min; h = max === r ? (g - b) / d + (g < b ? 6 : 0) : max === g ? (b - r) / d + 2 : (r - g) / d + 4; h *= 60; } return h; } } // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', () => { window.grooveApp = new GrooveApp(); });