kai-lofi-focus-timer / lightning.js
Elysia-Suite's picture
Upload 6 files
d804252 verified
/**
* 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.5, // Trigger lightning on strong beats (reduced from 0.7)
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]);
});
}
// Update stars ⭐
if (stars) {
stars.forEach(star => {
star.material.color.setHex(currentPalette.accent);
});
}
// Update halos 🔮
if (halos) {
halos.forEach(halo => {
halo.material.color.setHex(currentPalette.glow);
});
}
}
// ═══════════════════════════════════════════════════════════════════════
// 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
};
// Cache for MediaElementSourceNodes — an element can only be connected ONCE
const mediaSourceCache = new WeakMap();
// Connect to an audio element (radio or ambient)
window.connectAudioVisualizer = function (audioElement) {
if (!audioElement) return;
try {
// Disconnect previous source from analyser (but don't destroy it)
if (audioAnalyzer.source && audioAnalyzer.analyser) {
try {
audioAnalyzer.source.disconnect(audioAnalyzer.analyser);
} catch (e) {}
}
// 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
if (!audioAnalyzer.analyser) {
audioAnalyzer.analyser = audioAnalyzer.context.createAnalyser();
audioAnalyzer.analyser.fftSize = 256;
audioAnalyzer.analyser.smoothingTimeConstant = 0.8;
audioAnalyzer.analyser.connect(audioAnalyzer.context.destination);
}
// Get or create MediaElementSourceNode (can only be created ONCE per element!)
let source = mediaSourceCache.get(audioElement);
if (!source) {
source = audioAnalyzer.context.createMediaElementSource(audioElement);
mediaSourceCache.set(audioElement, source);
console.log("🎵 Created new MediaElementSource for audio element");
}
// Connect source to analyser
source.connect(audioAnalyzer.analyser);
audioAnalyzer.source = source;
// Prepare data arrays
if (!audioAnalyzer.frequencyData) {
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 && audioAnalyzer.analyser) {
try {
// Only disconnect from analyser, don't destroy the source
// (MediaElementSourceNode can't be recreated for the same element)
audioAnalyzer.source.disconnect(audioAnalyzer.analyser);
} 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);
}
// ═══════════════════════════════════════════════════════════════════════
// STARS ⭐ (Twinkling star shapes)
// ═══════════════════════════════════════════════════════════════════════
let stars = [];
function createStarShape() {
const shape = new THREE.Shape();
const outerRadius = 1;
const innerRadius = 0.4;
const points = 5;
for (let i = 0; i < points * 2; i++) {
const radius = i % 2 === 0 ? outerRadius : innerRadius;
const angle = (i * Math.PI) / points;
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
if (i === 0) shape.moveTo(x, y);
else shape.lineTo(x, y);
}
shape.closePath();
return new THREE.ShapeGeometry(shape);
}
function createStars() {
const starCount = 15;
const starGeometry = createStarShape();
for (let i = 0; i < starCount; i++) {
const material = new THREE.MeshBasicMaterial({
color: currentPalette.accent,
transparent: true,
opacity: 0.6,
side: THREE.DoubleSide,
blending: THREE.AdditiveBlending
});
const star = new THREE.Mesh(starGeometry, material);
star.position.set((Math.random() - 0.5) * 80, (Math.random() - 0.5) * 60, (Math.random() - 0.5) * 30 - 10);
star.rotation.z = Math.random() * Math.PI * 2;
star.userData = {
twinkleSpeed: 0.5 + Math.random() * 1.5,
twinklePhase: Math.random() * Math.PI * 2,
rotationSpeed: (Math.random() - 0.5) * 0.01
};
scene.add(star);
stars.push(star);
}
}
function updateStars() {
if (!stars.length) return;
const time = Date.now() * 0.001;
const audioBoost = 1 + audioAnalyzer.averageLevel * 0.5;
stars.forEach(star => {
// Twinkle effect
const twinkle = Math.sin(time * star.userData.twinkleSpeed + star.userData.twinklePhase);
star.material.opacity = 0.3 + twinkle * 0.3 + audioAnalyzer.highLevel * 0.4;
// Gentle rotation
star.rotation.z += star.userData.rotationSpeed * audioBoost;
// Scale pulse with bass
const scale = 1 + audioAnalyzer.bassLevel * 0.3;
star.scale.setScalar(scale);
});
}
// ═══════════════════════════════════════════════════════════════════════
// HALOS 🔮 (Pulsing rings of light)
// ═══════════════════════════════════════════════════════════════════════
let halos = [];
function createHalos() {
const haloCount = 3;
for (let i = 0; i < haloCount; i++) {
const geometry = new THREE.RingGeometry(8 + i * 5, 9 + i * 5, 32);
const material = new THREE.MeshBasicMaterial({
color: currentPalette.glow,
transparent: true,
opacity: 0.1,
side: THREE.DoubleSide,
blending: THREE.AdditiveBlending
});
const halo = new THREE.Mesh(geometry, material);
halo.position.set(0, 0, -20 - i * 5);
halo.userData = {
baseScale: 1,
pulseSpeed: 0.8 + i * 0.3,
pulsePhase: i * Math.PI * 0.5
};
scene.add(halo);
halos.push(halo);
}
}
function updateHalos() {
if (!halos.length) return;
const time = Date.now() * 0.001;
const bassBoost = 1 + audioAnalyzer.bassLevel * 2;
halos.forEach((halo, i) => {
// Pulse effect
const pulse = Math.sin(time * halo.userData.pulseSpeed + halo.userData.pulsePhase);
const scale = halo.userData.baseScale + pulse * 0.15 * bassBoost;
halo.scale.setScalar(scale);
// Opacity pulse
halo.material.opacity = 0.05 + pulse * 0.05 + audioAnalyzer.averageLevel * 0.15;
// Gentle rotation
halo.rotation.z += 0.001 * (i + 1);
});
}
// ═══════════════════════════════════════════════════════════════════════
// 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 (with mesh pooling to reduce GC pressure)
// ═══════════════════════════════════════════════════════════════════════
let activeLightning = [];
let lightningPool = []; // Reusable meshes
const MAX_POOL_SIZE = 20;
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);
// Return to pool instead of destroying (reduce GC pressure)
if (lightningPool.length < MAX_POOL_SIZE) {
bolt.visible = false;
lightningPool.push(bolt);
} else {
// Pool is full, dispose
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);
}
// Reusable flash element (created once, reused)
let flashElement = null;
function flashScreen() {
// Create flash element once and reuse
if (!flashElement) {
flashElement = document.createElement("div");
flashElement.style.cssText = `
position: fixed;
inset: 0;
background: rgba(59, 130, 246, 0.1);
pointer-events: none;
z-index: 9999;
opacity: 0;
transition: opacity 0.15s ease-out;
`;
document.body.appendChild(flashElement);
}
// Trigger flash via opacity
flashElement.style.opacity = "1";
requestAnimationFrame(() => {
requestAnimationFrame(() => {
flashElement.style.opacity = "0";
});
});
}
// 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
// ═══════════════════════════════════════════════════════════════════════
let isAnimating = true;
let animationFrameId = null;
function animate() {
if (!isAnimating) return; // Pause rendering when disabled
animationFrameId = requestAnimationFrame(animate);
// Update audio analysis 🎵
updateAudioAnalysis();
// Update particles
if (CONFIG.particles.enabled) {
updateParticles();
}
// Update floating orbs ✨
if (CONFIG.effects.floatingOrbs) {
updateOrbs();
}
// Update stars ⭐
updateStars();
// Update halos 🔮
updateHalos();
// 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);
}
// Expose controls for external toggle
window.pauseVisualEffects = function () {
isAnimating = false;
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
console.log("⚡ Visual effects paused (CPU saved!)");
};
window.resumeVisualEffects = function () {
if (!isAnimating) {
isAnimating = true;
animate();
console.log("⚡ Visual effects resumed");
}
};
// ═══════════════════════════════════════════════════════════════════════
// 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();
}
// Create stars ⭐
createStars();
// Create halos 🔮
createHalos();
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("⭐ Twinkling stars added!");
console.log("🔮 Pulsing halos activated!");
console.log("🎵 Audio visualizer ready — connect radio to make particles dance!");
console.log("🎨 Color palettes: focus (blue), short (green), long (purple)");
}
init();
})();