/* ═══════════════════════════════════════════════════════════════════════════ Rain Effect — canvas-based, matches ReVoltVPN Flutter RainEffect ~150 drops, white/light with slight leftward slant, semi-transparent Recycles drops to the top when they fall off-screen. ═══════════════════════════════════════════════════════════════════════════ */ (function () { const canvas = document.getElementById('bg-rain'); if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; /* ── Configuration (mirrors RainEffect defaults) ────────────────────── */ const DROP_COUNT = 150; const COLOR = 'rgba(232, 234, 246, {opacity})'; // matches #E8EAF6 const MIN_LENGTH = 12; const MAX_LENGTH = 44; const WIND_DRIFT = 4; // slight leftward slant in px const STROKE_W = 1.5; /* ── State ──────────────────────────────────────────────────────────── */ const drops = []; let width = 0; let height = 0; let lastTime = 0; let animId = null; /* ── Drop model ─────────────────────────────────────────────────────── */ class Drop { constructor() { this.spawn(); } /** Reset all properties to a new random drop. */ spawn() { this.x = Math.random(); // 0–1 normalized this.y = Math.random(); // 0–1 normalized this.speed = 0.30 + Math.random() * 0.55; // matches Flutter range this.length = MIN_LENGTH + Math.random() * (MAX_LENGTH - MIN_LENGTH); this.opacity = 0.05 + Math.random() * 0.12; // matches Flutter range } } /* ── Initialize drops ────────────────────────────────────────────────── */ for (let i = 0; i < DROP_COUNT; i++) { drops.push(new Drop()); } /* ── Resize handler ──────────────────────────────────────────────────── */ function resize() { const dpr = window.devicePixelRatio || 1; width = window.innerWidth; height = window.innerHeight; canvas.width = width * dpr; canvas.height = height * dpr; canvas.style.width = width + 'px'; canvas.style.height = height + 'px'; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } /* ── Animation loop ──────────────────────────────────────────────────── */ function tick(now) { animId = requestAnimationFrame(tick); /* Delta time in seconds, clamped */ let dt = 0.016; // ~60fps fallback if (lastTime !== 0) { dt = (now - lastTime) / 1000; if (dt > 0.1) dt = 0.1; } lastTime = now; /* Update drops */ for (const d of drops) { d.y += d.speed * dt; if (d.y > 1.08) { d.spawn(); d.y = -0.06 - Math.random() * 0.08; // start slightly above viewport } } /* Draw */ ctx.clearRect(0, 0, width, height); ctx.lineCap = 'round'; ctx.lineWidth = STROKE_W; for (const d of drops) { const sx = d.x * width; const sy = d.y * height; const ex = sx - WIND_DRIFT; // slight leftward slant const ey = sy + d.length; ctx.strokeStyle = COLOR.replace('{opacity}', d.opacity.toFixed(3)); ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(ex, ey); ctx.stroke(); } } /* ── Bootstrap ───────────────────────────────────────────────────────── */ resize(); window.addEventListener('resize', resize); animId = requestAnimationFrame(tick); })();