/** * Particles — Canvas floating math symbols background * Full-viewport canvas with slow-drifting math symbols (Σ, π, ∫, ∞, Δ, √, α, β) * Respects prefers-reduced-motion, disables on viewports < 768px, caps at 30 FPS. */ const Particles = (() => { let canvas = null; let ctx = null; let particles = []; let animId = null; let lastTime = 0; const FPS = 30; const FRAME_TIME = 1000 / FPS; const SYMBOLS = ['Σ', 'π', '∫', '∞', 'Δ', '√', 'α', 'β', 'λ', 'μ', '∂', '∑']; function prefersReducedMotion() { return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; } function randomBetween(a, b) { return a + Math.random() * (b - a); } class Particle { constructor() { this.reset(); } reset() { this.x = Math.random() * (canvas ? canvas.width : window.innerWidth); this.y = Math.random() * (canvas ? canvas.height : window.innerHeight); this.size = randomBetween(16, 32); this.symbol = SYMBOLS[Math.floor(Math.random() * SYMBOLS.length)]; this.vx = randomBetween(-0.2, 0.2); this.vy = randomBetween(-0.15, 0.15); this.rotation = randomBetween(0, Math.PI * 2); this.rotationSpeed = randomBetween(-0.005, 0.005); this.opacity = randomBetween(0.4, 0.8); } update() { this.x += this.vx; this.y += this.vy; this.rotation += this.rotationSpeed; const W = canvas ? canvas.width : window.innerWidth; const H = canvas ? canvas.height : window.innerHeight; if (this.x < -50) this.x = W + 50; if (this.x > W + 50) this.x = -50; if (this.y < -50) this.y = H + 50; if (this.y > H + 50) this.y = -50; } draw(ctx) { ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.rotation); ctx.font = `${this.size}px "Times New Roman", serif`; ctx.fillStyle = `rgba(108, 92, 231, ${this.opacity})`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(this.symbol, 0, 0); ctx.restore(); } } function initCanvas() { if (window.innerWidth < 768) return; if (prefersReducedMotion()) return; canvas = document.getElementById('particles-bg'); if (!canvas) { canvas = document.createElement('canvas'); canvas.id = 'particles-bg'; document.body.appendChild(canvas); } ctx = canvas.getContext('2d'); resize(); window.addEventListener('resize', resize); // Create particles const count = Math.min(40, Math.floor((window.innerWidth * window.innerHeight) / 25000)); particles = []; for (let i = 0; i < count; i++) { particles.push(new Particle()); } lastTime = performance.now(); loop(performance.now()); } function resize() { if (!canvas) return; canvas.width = window.innerWidth; canvas.height = window.innerHeight; } function loop(timestamp) { animId = requestAnimationFrame(loop); if (timestamp - lastTime < FRAME_TIME) return; lastTime = timestamp; if (!ctx || !canvas) return; ctx.clearRect(0, 0, canvas.width, canvas.height); for (const p of particles) { p.update(); p.draw(ctx); } } function destroy() { if (animId) { cancelAnimationFrame(animId); animId = null; } if (canvas && canvas.parentElement) { canvas.parentElement.removeChild(canvas); } canvas = null; ctx = null; particles = []; window.removeEventListener('resize', resize); } // Auto-init on load if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initCanvas); } else { initCanvas(); } return { init: initCanvas, destroy }; })();