/** * 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(); })();