/* ═══════════════════════════════════════════════════════════════════════════ Lightning Effect — canvas-based, matches ReVoltVPN Flutter LightningEffect Random jagged yellow bolts with glow, 2-10 sec intervals, double-strike ═══════════════════════════════════════════════════════════════════════════ */ (function () { const canvas = document.getElementById('bg-lightning'); if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; /* ── Configuration (mirrors LightningEffect defaults) ────────────────── */ const MIN_INTERVAL_SEC = 2; const MAX_INTERVAL_SEC = 10; const DOUBLE_CHANCE = 0.2; /* ── State ──────────────────────────────────────────────────────────── */ let width = 0; let height = 0; let bolt = null; // current LightningBolt or null let opacity = 0; // current opacity (driven by easing) let startTime = 0; // performance.now() at strike start const DURATION_MS = 400; // total animation duration let animId = null; let timerId = null; /* ── LightningBolt model ──────────────────────────────────────────────── */ class LightningBolt { /** * @param {Array<{x:number, y:number}>} mainPath — zigzag from top to bottom * @param {Array>} branches — fork paths */ constructor(mainPath, branches = []) { this.mainPath = mainPath; this.branches = branches; } /** Generate a random bolt — matches Flutter LightningBolt.random(). */ static random() { const points = []; // Start: top, slight horizontal offset let x = 0.5 + (Math.random() - 0.5) * 0.15; points.push({ x, y: 0.0 }); // Zigzag down — 6-10 segments const segments = 6 + Math.floor(Math.random() * 5); for (let i = 1; i < segments; i++) { const y = i / segments; x += (Math.random() - 0.5) * 0.25; x = clamp(x, 0.05, 0.95); points.push({ x, y }); } // End: bottom points.push({ x: clamp(x, 0.1, 0.9), y: 1.0 }); // Branches — 0-2 forks const branches = []; const branchCount = Math.floor(Math.random() * 3); for (let b = 0; b < branchCount; b++) { const branch = []; const startIdx = 1 + Math.floor(Math.random() * (points.length - 2)); const start = points[startIdx]; branch.push({ ...start }); let bx = start.x; let by = start.y; const branchLen = 2 + Math.floor(Math.random() * 3); for (let j = 0; j < branchLen; j++) { by += 0.05 + Math.random() * 0.12; bx += (Math.random() - 0.5) * 0.3; bx = clamp(bx, 0.02, 0.98); by = clamp(by, 0.0, 1.0); branch.push({ x: bx, y: by }); } branches.push(branch); } return new LightningBolt(points, branches); } } /* ── Easing: sharp flash curve (fast on, fast off, no slow fade) ────── */ function flashOpacity(t) { if (t < 0.08) return t / 0.08; // fast ramp 0→1 in 8% of time return 1.0 - ((t - 0.08) / 0.92); // linear decay over rest of duration } /* ── Schedule next strike ────────────────────────────────────────────── */ function scheduleNext() { const secs = MIN_INTERVAL_SEC + Math.floor(Math.random() * (MAX_INTERVAL_SEC - MIN_INTERVAL_SEC + 1)); timerId = setTimeout(strike, secs * 1000); } /* ── Strike ──────────────────────────────────────────────────────────── */ function strike() { bolt = LightningBolt.random(); startTime = performance.now(); // Double strike? if (Math.random() < DOUBLE_CHANCE) { setTimeout(() => { bolt = LightningBolt.random(); startTime = performance.now(); }, 120); } scheduleNext(); } /* ── 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); } /* ── Draw a single path (main or branch) ─────────────────────────────── */ function drawPath(pts, px, pn) { if (pts.length < 2) return; ctx.beginPath(); ctx.moveTo(pts[0].x * width, pts[0].y * height); for (let i = 1; i < pts.length; i++) { ctx.lineTo(pts[i].x * width, pts[i].y * height); } ctx.stroke(); } /* ── Render frame ────────────────────────────────────────────────────── */ function render() { if (!bolt) { opacity = 0; return; } const elapsed = performance.now() - startTime; const t = Math.min(elapsed / DURATION_MS, 1.0); opacity = flashOpacity(t); if (opacity <= 0.02) { bolt = null; opacity = 0; return; } } /* ── Animation loop ──────────────────────────────────────────────────── */ function tick() { animId = requestAnimationFrame(tick); render(); ctx.clearRect(0, 0, width, height); if (!bolt || opacity <= 0.02) return; const baseAlpha = Math.round(opacity * 255); const glowAlpha = Math.round(opacity * 0.35 * 255); // Glow layer — wide, blurred ctx.save(); ctx.strokeStyle = `rgba(255, 214, 0, ${(glowAlpha / 255).toFixed(3)})`; ctx.lineWidth = 18; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.shadowColor = `rgba(255, 214, 0, ${(glowAlpha / 255).toFixed(3)})`; ctx.shadowBlur = 8; drawPath(bolt.mainPath); for (const b of bolt.branches) drawPath(b); ctx.restore(); // Core — medium ctx.strokeStyle = `rgba(255, 214, 0, ${(baseAlpha / 255).toFixed(3)})`; ctx.lineWidth = 5; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.shadowColor = 'transparent'; ctx.shadowBlur = 0; drawPath(bolt.mainPath); for (const b of bolt.branches) drawPath(b); // Hot center — white, only when bright if (opacity > 0.6) { const hotAlpha = Math.round(opacity * 180); ctx.strokeStyle = `rgba(255, 255, 255, ${(hotAlpha / 255).toFixed(3)})`; ctx.lineWidth = 2; drawPath(bolt.mainPath); for (const b of bolt.branches) drawPath(b); } } /* ── Helpers ──────────────────────────────────────────────────────────── */ function clamp(v, min, max) { return v < min ? min : v > max ? max : v; } /* ── Bootstrap ────────────────────────────────────────────────────────── */ resize(); window.addEventListener('resize', resize); animId = requestAnimationFrame(tick); scheduleNext(); })();