kai-lofi-focus-timer / lightning.js
Elysia-Suite's picture
Upload 7 files
e1301d4 verified
raw
history blame
25.4 kB
/**
* 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();
})();