Spaces:
Running
Running
| // 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(); | |
| }); |