Spaces:
Running
Running
| const canvas = document.getElementById('particleCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| // Configuration | |
| let config = { | |
| particleCount: 100, | |
| particleSize: 2, | |
| speed: 1, | |
| connectionDistance: 100, | |
| colorMode: 'rainbow', | |
| mouseEffect: 'attract', | |
| mouseRadius: 150 | |
| }; | |
| // Mouse position | |
| let mouse = { | |
| x: canvas.width / 2, | |
| y: canvas.height / 2 | |
| }; | |
| // Particles array | |
| let particles = []; | |
| // Color modes | |
| const colorModes = { | |
| rainbow: (particle, time) => { | |
| const hue = (time * 0.1 + particle.id * 10) % 360; | |
| return `hsl(${hue}, 70%, 50%)`; | |
| }, | |
| ocean: (particle, time) => { | |
| const hue = 180 + Math.sin(time * 0.001 + particle.id * 0.1) * 60; | |
| return `hsl(${hue}, 70%, 50%)`; | |
| }, | |
| fire: (particle, time) => { | |
| const hue = Math.sin(time * 0.001 + particle.id * 0.1) * 30; | |
| return `hsl(${hue}, 80%, 50%)`; | |
| }, | |
| monochrome: (particle, time) => { | |
| const brightness = 30 + Math.sin(time * 0.001 + particle.id * 0.1) * 20; | |
| return `hsl(0, 0%, ${brightness}%)`; | |
| }, | |
| neon: (particle, time) => { | |
| const colors = ['#ff00ff', '#00ffff', '#ffff00', '#ff00aa']; | |
| return colors[Math.floor((time * 0.001 + particle.id * 0.1) % colors.length)]; | |
| } | |
| }; | |
| // Particle class | |
| class Particle { | |
| constructor(id) { | |
| this.id = id; | |
| this.x = Math.random() * canvas.width; | |
| this.y = Math.random() * canvas.height; | |
| this.vx = (Math.random() - 0.5) * config.speed; | |
| this.vy = (Math.random() - 0.5) * config.speed; | |
| this.size = config.particleSize; | |
| this.baseSize = config.particleSize; | |
| } | |
| update() { | |
| // Mouse interaction | |
| const dx = mouse.x - this.x; | |
| const dy = mouse.y - this.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < config.mouseRadius) { | |
| const force = (1 - distance / config.mouseRadius) * 0.5; | |
| if (config.mouseEffect === 'attract') { | |
| this.vx += dx * force * 0.01; | |
| this.vy += dy * force * 0.01; | |
| } else { | |
| this.vx -= dx * force * 0.01; | |
| this.vy -= dy * force * 0.01; | |
| } | |
| } | |
| // Update position | |
| this.x += this.vx * config.speed; | |
| this.y += this.vy * config.speed; | |
| // Bounce off walls | |
| if (this.x < 0 || this.x > canvas.width) { | |
| this.vx *= -0.9; | |
| this.x = Math.max(0, Math.min(canvas.width, this.x)); | |
| } | |
| if (this.y < 0 || this.y > canvas.height) { | |
| this.vy *= -0.9; | |
| this.y = Math.max(0, Math.min(canvas.height, this.y)); | |
| } | |
| // Apply friction | |
| this.vx *= 0.99; | |
| this.vy *= 0.99; | |
| // Size animation | |
| this.size = this.baseSize + Math.sin(Date.now() * 0.001 + this.id) * 0.5; | |
| } | |
| draw(time) { | |
| ctx.fillStyle = colorModes[config.colorMode](this, time); | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Glow effect | |
| ctx.shadowBlur = 10; | |
| ctx.shadowColor = colorModes[config.colorMode](this, time); | |
| ctx.fill(); | |
| ctx.shadowBlur = 0; | |
| } | |
| } | |
| // Initialize particles | |
| function initParticles() { | |
| particles = []; | |
| for (let i = 0; i < config.particleCount; i++) { | |
| particles.push(new Particle(i)); | |
| } | |
| } | |
| // Draw connections between particles | |
| function drawConnections() { | |
| for (let i = 0; i < particles.length; i++) { | |
| for (let j = i + 1; j < particles.length; j++) { | |
| const dx = particles[i].x - particles[j].x; | |
| const dy = particles[i].y - particles[j].y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < config.connectionDistance) { | |
| const opacity = 1 - distance / config.connectionDistance; | |
| ctx.strokeStyle = `rgba(100, 255, 218, ${opacity * 0.2})`; | |
| ctx.lineWidth = opacity * 0.5; | |
| ctx.beginPath(); | |
| ctx.moveTo(particles[i].x, particles[i].y); | |
| ctx.lineTo(particles[j].x, particles[j].y); | |
| ctx.stroke(); | |
| } | |
| } | |
| } | |
| } | |
| // Animation loop | |
| function animate() { | |
| ctx.fillStyle = 'rgba(10, 10, 10, 0.1)'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| const time = Date.now(); | |
| // Update and draw particles | |
| particles.forEach(particle => { | |
| particle.update(); | |
| particle.draw(time); | |
| }); | |
| // Draw connections | |
| drawConnections(); | |
| requestAnimationFrame(animate); | |
| } | |
| // Event listeners | |
| window.addEventListener('resize', () => { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| }); | |
| canvas.addEventListener('mousemove', (e) => { | |
| mouse.x = e.clientX; | |
| mouse.y = e.clientY; | |
| }); | |
| canvas.addEventListener('click', (e) => { | |
| // Create explosion effect | |
| for (let i = 0; i < 20; i++) { | |
| const angle = (Math.PI * 2 * i) / 20; | |
| const speed = 5 + Math.random() * 5; | |
| const particle = new Particle(particles.length); | |
| particle.x = e.clientX; | |
| particle.y = e.clientY; | |
| particle.vx = Math.cos(angle) * speed; | |
| particle.vy = Math.sin(angle) * speed; | |
| particle.baseSize = 3 + Math.random() * 2; | |
| particles.push(particle); | |
| } | |
| // Remove excess particles | |
| if (particles.length > config.particleCount * 2) { | |
| particles.splice(0, particles.length - config.particleCount); | |
| } | |
| }); | |
| // Keyboard controls | |
| document.addEventListener('keydown', (e) => { | |
| switch(e.key.toLowerCase()) { | |
| case ' ': | |
| e.preventDefault(); | |
| const modes = Object.keys(colorModes); | |
| const currentIndex = modes.indexOf(config.colorMode); | |
| config.colorMode = modes[(currentIndex + 1) % modes.length]; | |
| break; | |
| case '1': | |
| config.particleCount = 50; | |
| initParticles(); | |
| break; | |
| case '2': | |
| config.particleCount = 100; | |
| initParticles(); | |
| break; | |
| case '3': | |
| config.particleCount = 200; | |
| initParticles(); | |
| break; | |
| case '4': | |
| config.particleCount = 300; | |
| initParticles(); | |
| break; | |
| case '5': | |
| config.particleCount = 500; | |
| initParticles(); | |
| break; | |
| case 'q': | |
| config.speed = Math.max(0.1, config.speed - 0.1); | |
| break; | |
| case 'w': | |
| config.speed = Math.min(5, config.speed + 0.1); | |
| break; | |
| case 'a': | |
| config.particleSize = Math.max(1, config.particleSize - 0.5); | |
| particles.forEach(p => p.baseSize = config.particleSize); | |
| break; | |
| case 's': | |
| config.particleSize = Math.min(10, config.particleSize + 0.5); | |
| particles.forEach(p => p.baseSize = config.particleSize); | |
| break; | |
| case 'z': | |
| config.connectionDistance = Math.max(50, config.connectionDistance - 10); | |
| break; | |
| case 'x': | |
| config.connectionDistance = Math.min(300, config.connectionDistance + 10); | |
| break; | |
| case 'r': | |
| config = { | |
| particleCount: 100, | |
| particleSize: 2, | |
| speed: 1, | |
| connectionDistance: 100, | |
| colorMode: 'rainbow', | |
| mouseEffect: 'attract', | |
| mouseRadius: 150 | |
| }; | |
| initParticles(); | |
| break; | |
| case 'm': | |
| config.mouseEffect = config.mouseEffect === 'attract' ? 'repel' : 'attract'; | |
| break; | |
| } | |
| }); | |
| // Initialize and start | |
| initParticles(); | |
| animate(); |