Spaces:
Running
Running
| /** | |
| * Lightning Effects — Three.js Magic ⚡ | |
| * ═══════════════════════════════════════════════════════════════════════════ | |
| * Procedural lightning bolts + floating particles + AUDIO REACTIVE! 🎵 | |
| * Inspired by Ivy's audio visualizer 🌿 | |
| * Made with 💙 by Kai | |
| * ═══════════════════════════════════════════════════════════════════════════ | |
| */ | |
| (function () { | |
| "use strict"; | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| // CONFIGURATION | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| const CONFIG = { | |
| // Lightning | |
| lightning: { | |
| enabled: true, | |
| minInterval: 4000, // Min time between strikes (ms) | |
| maxInterval: 12000, // Max time between strikes (ms) | |
| duration: 200, // How long the bolt stays visible (ms) | |
| branches: 3, // Number of branch segments | |
| color: 0x3b82f6, // Electric blue | |
| glowColor: 0x60a5fa, // Lighter blue for glow | |
| intensity: 2.5 | |
| }, | |
| // Particles | |
| particles: { | |
| enabled: true, | |
| count: 120, // More particles! | |
| size: 2.5, | |
| baseSpeed: 0.15, | |
| color: 0x3b82f6, | |
| opacity: 0.5 | |
| }, | |
| // Color Palettes 🎨 | |
| palettes: { | |
| focus: { | |
| primary: 0x3b82f6, // Electric blue | |
| secondary: 0x8b5cf6, // Purple | |
| accent: 0x06b6d4, // Cyan | |
| glow: 0x60a5fa | |
| }, | |
| short: { | |
| primary: 0x10b981, // Emerald green | |
| secondary: 0x34d399, // Light green | |
| accent: 0x6ee7b7, // Mint | |
| glow: 0x34d399 | |
| }, | |
| long: { | |
| primary: 0x8b5cf6, // Purple | |
| secondary: 0xa855f7, // Light purple | |
| accent: 0xc084fc, // Lavender | |
| glow: 0xa855f7 | |
| } | |
| }, | |
| // Audio Reactive 🎵 | |
| audio: { | |
| enabled: true, | |
| beatThreshold: 0.7, // Trigger lightning on strong beats | |
| particleReactivity: 2.0, // How much particles react | |
| colorShift: true // Shift colors with music | |
| }, | |
| // Effects ✨ | |
| effects: { | |
| floatingOrbs: true, // Glowing orbs | |
| trailParticles: true, // Trailing effect | |
| pulsingGlow: true, // Ambient pulsing | |
| rainbowMode: false // Party mode! | |
| }, | |
| // Performance | |
| pixelRatio: Math.min(window.devicePixelRatio, 2) | |
| }; | |
| // Current color palette (changes with timer mode) | |
| let currentPalette = CONFIG.palettes.focus; | |
| let currentMode = "focus"; | |
| // Update palette based on timer mode | |
| window.setVisualizerMode = function (mode) { | |
| currentMode = mode; | |
| currentPalette = CONFIG.palettes[mode] || CONFIG.palettes.focus; | |
| updateSceneColors(); | |
| console.log(`✨ Visualizer mode: ${mode}`); | |
| }; | |
| function updateSceneColors() { | |
| // Update particles color | |
| if (particles && particles.material) { | |
| particles.material.color.setHex(currentPalette.primary); | |
| } | |
| // Update orbs | |
| if (orbs) { | |
| orbs.forEach((orb, i) => { | |
| const colors = [currentPalette.primary, currentPalette.secondary, currentPalette.accent]; | |
| orb.material.color.setHex(colors[i % 3]); | |
| }); | |
| } | |
| } | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| // AUDIO ANALYZER 🎵 (Inspired by Ivy 🌿) | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| let audioAnalyzer = { | |
| context: null, | |
| analyser: null, | |
| source: null, | |
| frequencyData: null, | |
| timeDomainData: null, | |
| isConnected: false, | |
| lastBeatTime: 0, | |
| bassLevel: 0, | |
| midLevel: 0, | |
| highLevel: 0, | |
| averageLevel: 0 | |
| }; | |
| // Connect to an audio element (radio or ambient) | |
| window.connectAudioVisualizer = function (audioElement) { | |
| if (!audioElement || audioAnalyzer.isConnected) return; | |
| try { | |
| // Create or reuse AudioContext | |
| if (!audioAnalyzer.context) { | |
| audioAnalyzer.context = new (window.AudioContext || window.webkitAudioContext)(); | |
| } | |
| // Resume if suspended | |
| if (audioAnalyzer.context.state === "suspended") { | |
| audioAnalyzer.context.resume(); | |
| } | |
| // Create analyser | |
| audioAnalyzer.analyser = audioAnalyzer.context.createAnalyser(); | |
| audioAnalyzer.analyser.fftSize = 256; | |
| audioAnalyzer.analyser.smoothingTimeConstant = 0.8; | |
| // Connect source | |
| audioAnalyzer.source = audioAnalyzer.context.createMediaElementSource(audioElement); | |
| audioAnalyzer.source.connect(audioAnalyzer.analyser); | |
| audioAnalyzer.analyser.connect(audioAnalyzer.context.destination); | |
| // Prepare data arrays | |
| audioAnalyzer.frequencyData = new Uint8Array(audioAnalyzer.analyser.frequencyBinCount); | |
| audioAnalyzer.timeDomainData = new Uint8Array(audioAnalyzer.analyser.fftSize); | |
| audioAnalyzer.isConnected = true; | |
| console.log("🎵 Audio visualizer connected! Particles will dance ⚡"); | |
| } catch (e) { | |
| console.log("🎵 Could not connect audio visualizer:", e.message); | |
| } | |
| }; | |
| window.disconnectAudioVisualizer = function () { | |
| if (audioAnalyzer.source) { | |
| try { | |
| audioAnalyzer.source.disconnect(); | |
| } catch (e) {} | |
| } | |
| audioAnalyzer.isConnected = false; | |
| audioAnalyzer.source = null; | |
| console.log("🎵 Audio visualizer disconnected"); | |
| }; | |
| function updateAudioAnalysis() { | |
| if (!audioAnalyzer.isConnected || !audioAnalyzer.analyser) { | |
| audioAnalyzer.bassLevel = 0; | |
| audioAnalyzer.midLevel = 0; | |
| audioAnalyzer.highLevel = 0; | |
| audioAnalyzer.averageLevel = 0; | |
| return; | |
| } | |
| audioAnalyzer.analyser.getByteFrequencyData(audioAnalyzer.frequencyData); | |
| const bins = audioAnalyzer.frequencyData.length; | |
| let bass = 0, | |
| mid = 0, | |
| high = 0; | |
| // Split frequency spectrum into bass/mid/high | |
| for (let i = 0; i < bins; i++) { | |
| const value = audioAnalyzer.frequencyData[i] / 255; | |
| if (i < bins * 0.15) { | |
| bass += value; // Low frequencies (bass) | |
| } else if (i < bins * 0.5) { | |
| mid += value; // Mid frequencies | |
| } else { | |
| high += value; // High frequencies | |
| } | |
| } | |
| // Normalize | |
| audioAnalyzer.bassLevel = bass / (bins * 0.15); | |
| audioAnalyzer.midLevel = mid / (bins * 0.35); | |
| audioAnalyzer.highLevel = high / (bins * 0.5); | |
| audioAnalyzer.averageLevel = (audioAnalyzer.bassLevel + audioAnalyzer.midLevel + audioAnalyzer.highLevel) / 3; | |
| // Beat detection — trigger lightning on strong bass hits! | |
| const now = Date.now(); | |
| if (audioAnalyzer.bassLevel > CONFIG.audio.beatThreshold && now - audioAnalyzer.lastBeatTime > 500) { | |
| audioAnalyzer.lastBeatTime = now; | |
| if (CONFIG.audio.enabled) { | |
| createLightningBolt(); // ⚡ Lightning on beat! | |
| } | |
| } | |
| } | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| // THREE.JS SETUP | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| const canvas = document.getElementById("lightning-canvas"); | |
| if (!canvas) return; | |
| // Scene | |
| const scene = new THREE.Scene(); | |
| // Camera | |
| const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.z = 50; | |
| // Renderer | |
| const renderer = new THREE.WebGLRenderer({ | |
| canvas: canvas, | |
| alpha: true, | |
| antialias: true | |
| }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(CONFIG.pixelRatio); | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| // FLOATING PARTICLES (lo-fi dust/stars) | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| let particles; | |
| let orbs = []; | |
| function createParticles() { | |
| const geometry = new THREE.BufferGeometry(); | |
| const positions = new Float32Array(CONFIG.particles.count * 3); | |
| const colors = new Float32Array(CONFIG.particles.count * 3); | |
| const velocities = []; | |
| for (let i = 0; i < CONFIG.particles.count; i++) { | |
| // Random position in view | |
| positions[i * 3] = (Math.random() - 0.5) * 100; // x | |
| positions[i * 3 + 1] = (Math.random() - 0.5) * 100; // y | |
| positions[i * 3 + 2] = (Math.random() - 0.5) * 50; // z | |
| // Rainbow colors for particles ✨ | |
| const hue = (i / CONFIG.particles.count) * 0.3 + 0.5; // Blue to purple range | |
| const color = new THREE.Color().setHSL(hue, 0.8, 0.6); | |
| colors[i * 3] = color.r; | |
| colors[i * 3 + 1] = color.g; | |
| colors[i * 3 + 2] = color.b; | |
| // Random velocity | |
| velocities.push({ | |
| x: (Math.random() - 0.5) * CONFIG.particles.baseSpeed, | |
| y: (Math.random() - 0.5) * CONFIG.particles.baseSpeed, | |
| z: (Math.random() - 0.5) * CONFIG.particles.baseSpeed * 0.5 | |
| }); | |
| } | |
| geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); | |
| geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3)); | |
| const material = new THREE.PointsMaterial({ | |
| size: CONFIG.particles.size, | |
| transparent: true, | |
| opacity: CONFIG.particles.opacity, | |
| blending: THREE.AdditiveBlending, | |
| vertexColors: true // Use per-vertex colors! | |
| }); | |
| particles = new THREE.Points(geometry, material); | |
| particles.userData.velocities = velocities; | |
| scene.add(particles); | |
| } | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| // FLOATING ORBS ✨ (Glowing spheres) | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| function createOrbs() { | |
| if (!CONFIG.effects.floatingOrbs) return; | |
| const orbCount = 5; | |
| const colors = [currentPalette.primary, currentPalette.secondary, currentPalette.accent, currentPalette.secondary, currentPalette.primary]; | |
| for (let i = 0; i < orbCount; i++) { | |
| const geometry = new THREE.SphereGeometry(1 + Math.random() * 2, 16, 16); | |
| const material = new THREE.MeshBasicMaterial({ | |
| color: colors[i], | |
| transparent: true, | |
| opacity: 0.15, | |
| blending: THREE.AdditiveBlending | |
| }); | |
| const orb = new THREE.Mesh(geometry, material); | |
| orb.position.set((Math.random() - 0.5) * 60, (Math.random() - 0.5) * 40, (Math.random() - 0.5) * 20 - 10); | |
| orb.userData = { | |
| baseY: orb.position.y, | |
| speed: 0.5 + Math.random() * 0.5, | |
| phase: Math.random() * Math.PI * 2 | |
| }; | |
| scene.add(orb); | |
| orbs.push(orb); | |
| } | |
| } | |
| function updateOrbs() { | |
| if (!orbs.length) return; | |
| const time = Date.now() * 0.001; | |
| const audioBoost = 1 + audioAnalyzer.averageLevel * 2; | |
| orbs.forEach((orb, i) => { | |
| // Floating motion | |
| orb.position.y = orb.userData.baseY + Math.sin(time * orb.userData.speed + orb.userData.phase) * 5; | |
| orb.position.x += Math.sin(time * 0.3 + i) * 0.02; | |
| // Pulse with audio | |
| const scale = (1 + Math.sin(time * 2 + i) * 0.2) * audioBoost; | |
| orb.scale.setScalar(scale); | |
| // Opacity pulse | |
| orb.material.opacity = 0.1 + Math.sin(time + i) * 0.05 + audioAnalyzer.bassLevel * 0.1; | |
| }); | |
| } | |
| function updateParticles() { | |
| if (!particles) return; | |
| const positions = particles.geometry.attributes.position.array; | |
| const colors = particles.geometry.attributes.color.array; | |
| const velocities = particles.userData.velocities; | |
| // Audio reactivity 🎵 | |
| const audioBoost = 1 + audioAnalyzer.averageLevel * CONFIG.audio.particleReactivity; | |
| const bassBoost = 1 + audioAnalyzer.bassLevel * 3; | |
| // Update particle size based on audio | |
| if (audioAnalyzer.isConnected) { | |
| particles.material.size = CONFIG.particles.size * bassBoost; | |
| particles.material.opacity = Math.min(0.8, CONFIG.particles.opacity + audioAnalyzer.midLevel * 0.4); | |
| // Color shift with high frequencies | |
| if (CONFIG.audio.colorShift && audioAnalyzer.highLevel > 0.3) { | |
| const hue = (Date.now() * 0.001 + audioAnalyzer.highLevel) % 1; | |
| particles.material.color.setHSL(0.6 + hue * 0.2, 0.8, 0.6); | |
| } else { | |
| particles.material.color.setHex(CONFIG.particles.color); | |
| } | |
| } | |
| for (let i = 0; i < CONFIG.particles.count; i++) { | |
| // Update position with audio-reactive speed | |
| const speedMultiplier = audioBoost; | |
| positions[i * 3] += velocities[i].x * speedMultiplier; | |
| positions[i * 3 + 1] += velocities[i].y * speedMultiplier; | |
| positions[i * 3 + 2] += velocities[i].z * speedMultiplier; | |
| // Shift colors with time for rainbow effect ✨ | |
| if (audioAnalyzer.isConnected && CONFIG.audio.colorShift) { | |
| const time = Date.now() * 0.0005; | |
| const hue = (i / CONFIG.particles.count + time) % 1; | |
| const color = new THREE.Color().setHSL(hue, 0.8, 0.5 + audioAnalyzer.averageLevel * 0.3); | |
| colors[i * 3] = color.r; | |
| colors[i * 3 + 1] = color.g; | |
| colors[i * 3 + 2] = color.b; | |
| } | |
| // Wrap around edges | |
| if (positions[i * 3] > 50) positions[i * 3] = -50; | |
| if (positions[i * 3] < -50) positions[i * 3] = 50; | |
| if (positions[i * 3 + 1] > 50) positions[i * 3 + 1] = -50; | |
| if (positions[i * 3 + 1] < -50) positions[i * 3 + 1] = 50; | |
| } | |
| particles.geometry.attributes.position.needsUpdate = true; | |
| if (audioAnalyzer.isConnected) { | |
| particles.geometry.attributes.color.needsUpdate = true; | |
| } | |
| } | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| // LIGHTNING BOLT GENERATION | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| let activeLightning = []; | |
| function createLightningBolt() { | |
| // Use current palette colors! 🎨 | |
| const boltColor = currentPalette.primary; | |
| const glowColor = currentPalette.glow; | |
| // Random start point (top area) | |
| const startX = (Math.random() - 0.5) * 80; | |
| const startY = 40 + Math.random() * 20; | |
| // Random end point (bottom area) | |
| const endX = startX + (Math.random() - 0.5) * 40; | |
| const endY = -40 - Math.random() * 20; | |
| // Generate main bolt path | |
| const points = generateBoltPath(startX, startY, endX, endY, 8); | |
| // Create main bolt | |
| const mainBolt = createBoltMesh(points, boltColor, 2); | |
| scene.add(mainBolt); | |
| activeLightning.push(mainBolt); | |
| // Create glow effect (thicker, more transparent) | |
| const glowBolt = createBoltMesh(points, glowColor, 6, 0.3); | |
| scene.add(glowBolt); | |
| activeLightning.push(glowBolt); | |
| // Create branches with accent color | |
| for (let i = 0; i < CONFIG.lightning.branches; i++) { | |
| const branchStart = Math.floor(Math.random() * (points.length - 2)) + 1; | |
| const branchPoint = points[branchStart]; | |
| const branchEndX = branchPoint.x + (Math.random() - 0.5) * 30; | |
| const branchEndY = branchPoint.y - 10 - Math.random() * 20; | |
| const branchPoints = generateBoltPath(branchPoint.x, branchPoint.y, branchEndX, branchEndY, 4); | |
| const branch = createBoltMesh(branchPoints, CONFIG.lightning.color, 1, 0.7); | |
| scene.add(branch); | |
| activeLightning.push(branch); | |
| } | |
| // Flash effect — briefly increase ambient | |
| flashScreen(); | |
| // Remove lightning after duration | |
| setTimeout(() => { | |
| activeLightning.forEach(bolt => { | |
| scene.remove(bolt); | |
| bolt.geometry.dispose(); | |
| bolt.material.dispose(); | |
| }); | |
| activeLightning = []; | |
| }, CONFIG.lightning.duration); | |
| console.log("⚡ Lightning strike!"); | |
| } | |
| function generateBoltPath(startX, startY, endX, endY, segments) { | |
| const points = []; | |
| points.push(new THREE.Vector3(startX, startY, 0)); | |
| const dx = (endX - startX) / segments; | |
| const dy = (endY - startY) / segments; | |
| for (let i = 1; i < segments; i++) { | |
| const x = startX + dx * i + (Math.random() - 0.5) * 15; | |
| const y = startY + dy * i + (Math.random() - 0.5) * 5; | |
| const z = (Math.random() - 0.5) * 5; | |
| points.push(new THREE.Vector3(x, y, z)); | |
| } | |
| points.push(new THREE.Vector3(endX, endY, 0)); | |
| return points; | |
| } | |
| function createBoltMesh(points, color, lineWidth, opacity = 1) { | |
| const geometry = new THREE.BufferGeometry().setFromPoints(points); | |
| const material = new THREE.LineBasicMaterial({ | |
| color: color, | |
| linewidth: lineWidth, | |
| transparent: true, | |
| opacity: opacity, | |
| blending: THREE.AdditiveBlending | |
| }); | |
| return new THREE.Line(geometry, material); | |
| } | |
| function flashScreen() { | |
| // Brief white flash overlay | |
| const flash = document.createElement("div"); | |
| flash.style.cssText = ` | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(59, 130, 246, 0.1); | |
| pointer-events: none; | |
| z-index: 9999; | |
| animation: flashFade 0.15s ease-out forwards; | |
| `; | |
| document.body.appendChild(flash); | |
| setTimeout(() => flash.remove(), 150); | |
| } | |
| // Add flash animation to document | |
| const style = document.createElement("style"); | |
| style.textContent = ` | |
| @keyframes flashFade { | |
| 0% { opacity: 1; } | |
| 100% { opacity: 0; } | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| // LIGHTNING SCHEDULER | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| function scheduleLightning() { | |
| if (!CONFIG.lightning.enabled) return; | |
| const delay = CONFIG.lightning.minInterval + Math.random() * (CONFIG.lightning.maxInterval - CONFIG.lightning.minInterval); | |
| setTimeout(() => { | |
| createLightningBolt(); | |
| scheduleLightning(); | |
| }, delay); | |
| } | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| // ANIMATION LOOP | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| // Update audio analysis 🎵 | |
| updateAudioAnalysis(); | |
| // Update particles | |
| if (CONFIG.particles.enabled) { | |
| updateParticles(); | |
| } | |
| // Update floating orbs ✨ | |
| if (CONFIG.effects.floatingOrbs) { | |
| updateOrbs(); | |
| } | |
| // Gentle camera sway (lo-fi vibe) — enhanced with audio | |
| const audioSway = audioAnalyzer.isConnected ? audioAnalyzer.bassLevel * 2 : 0; | |
| camera.position.x = Math.sin(Date.now() * 0.0001) * (2 + audioSway); | |
| camera.position.y = Math.cos(Date.now() * 0.00015) * (1 + audioSway * 0.5); | |
| renderer.render(scene, camera); | |
| } | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| // RESIZE HANDLER | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| function onResize() { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| window.addEventListener("resize", onResize); | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| // INITIALIZATION | |
| // ═══════════════════════════════════════════════════════════════════════ | |
| function init() { | |
| if (CONFIG.particles.enabled) { | |
| createParticles(); | |
| } | |
| if (CONFIG.effects.floatingOrbs) { | |
| createOrbs(); | |
| } | |
| if (CONFIG.lightning.enabled) { | |
| // First lightning after a short delay | |
| setTimeout(createLightningBolt, 2000); | |
| scheduleLightning(); | |
| } | |
| animate(); | |
| console.log("⚡ Lightning effects initialized!"); | |
| console.log("💙 Particles floating... lo-fi vibes activated"); | |
| console.log("✨ Floating orbs created!"); | |
| console.log("🎵 Audio visualizer ready — connect radio to make particles dance!"); | |
| console.log("🎨 Color palettes: focus (blue), short (green), long (purple)"); | |
| } | |
| init(); | |
| })(); | |