/** * 文件功能:聊天室雷电特效 * * 使用递归分叉算法叠加云层闪光、主闪电、余辉残影, * 在聊天室中模拟更有压迫感的雷暴闪电效果。 */ const LightningEffect = (() => { /** * 递归绘制闪电路径(分裂算法) * * @param {CanvasRenderingContext2D} ctx * @param {number} x1 起点 x * @param {number} y1 起点 y * @param {number} x2 终点 x * @param {number} y2 终点 y * @param {number} depth 当前递归深度(控制分叉层数) * @param {number} width 线条宽度 */ function _drawBolt(ctx, x1, y1, x2, y2, depth, width) { if (depth <= 0) return; // 中点随机偏移(越深层偏移越小,产生流畅感) const mx = (x1 + x2) / 2 + (Math.random() - 0.5) * 80 * depth; const my = (y1 + y2) / 2 + (Math.random() - 0.5) * 20 * depth; const glow = ctx.createLinearGradient(x1, y1, x2, y2); glow.addColorStop(0, "rgba(200, 220, 255, 0.9)"); glow.addColorStop(1, "rgba(150, 180, 255, 0.6)"); ctx.save(); ctx.strokeStyle = glow; ctx.lineWidth = width; ctx.shadowColor = "#aaccff"; ctx.shadowBlur = 18; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.quadraticCurveTo(mx, my, x2, y2); ctx.stroke(); ctx.restore(); // 递归绘制两段子路径 _drawBolt(ctx, x1, y1, mx, my, depth - 1, width * 0.65); _drawBolt(ctx, mx, my, x2, y2, depth - 1, width * 0.65); // 随机在中途分叉一条小支路(50% 概率) if (depth > 1 && Math.random() > 0.5) { const bx = mx + (Math.random() - 0.5) * 120; const by = my + Math.random() * 80 + 40; _drawBolt(ctx, mx, my, bx, by, depth - 2, width * 0.4); } } /** * 绘制顶部乌云压光层,让闪电更有“雷暴”氛围。 * * @param {HTMLCanvasElement} canvas * @param {CanvasRenderingContext2D} ctx */ function _drawStormGlow(canvas, ctx) { const w = canvas.width; const h = canvas.height; const sky = ctx.createLinearGradient(0, 0, 0, h * 0.8); sky.addColorStop(0, "rgba(7, 18, 38, 0.34)"); sky.addColorStop(0.45, "rgba(15, 23, 42, 0.18)"); sky.addColorStop(1, "rgba(15, 23, 42, 0)"); ctx.fillStyle = sky; ctx.fillRect(0, 0, w, h); for (let i = 0; i < 3; i++) { const cloudX = w * (0.12 + Math.random() * 0.76); const cloudY = h * (0.05 + Math.random() * 0.22); const cloudR = 120 + Math.random() * 160; const cloud = ctx.createRadialGradient(cloudX, cloudY, 0, cloudX, cloudY, cloudR); cloud.addColorStop(0, "rgba(210, 226, 255, 0.18)"); cloud.addColorStop(0.38, "rgba(168, 196, 255, 0.1)"); cloud.addColorStop(1, "rgba(168, 196, 255, 0)"); ctx.fillStyle = cloud; ctx.beginPath(); ctx.arc(cloudX, cloudY, cloudR, 0, Math.PI * 2); ctx.fill(); } } /** * 渲染一次闪电 + 闪屏效果。 * * @param {HTMLCanvasElement} canvas * @param {CanvasRenderingContext2D} ctx */ function _flash(canvas, ctx) { const w = canvas.width; const h = canvas.height; // 清空画布 ctx.clearRect(0, 0, w, h); // 先铺一层压暗天空,再叠加闪白,让明暗反差更明显。 _drawStormGlow(canvas, ctx); ctx.fillStyle = "rgba(228, 239, 255, 0.46)"; ctx.fillRect(0, 0, w, h); // 绘制 1-3 条主闪电,并给出明显的白色核心线。 const boltCount = Math.floor(Math.random() * 3) + 1; for (let i = 0; i < boltCount; i++) { const x1 = w * (0.12 + Math.random() * 0.76); const y1 = 0; const x2 = x1 + (Math.random() - 0.5) * 360; const y2 = h * (0.55 + Math.random() * 0.35); const width = 3.6 + Math.random() * 1.8; _drawBolt(ctx, x1, y1, x2, y2, 5, width); ctx.save(); ctx.strokeStyle = "rgba(255,255,255,0.92)"; ctx.lineWidth = Math.max(1.2, width * 0.34); ctx.shadowColor = "rgba(255,255,255,0.95)"; ctx.shadowBlur = 22; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); ctx.restore(); } // 短促残影:闪电消失后保留一层很淡的余辉,避免“闪一下就没了”。 setTimeout(() => { ctx.clearRect(0, 0, w, h); _drawStormGlow(canvas, ctx); ctx.fillStyle = "rgba(185, 205, 255, 0.12)"; ctx.fillRect(0, 0, w, h); }, 90); setTimeout(() => { ctx.clearRect(0, 0, w, h); }, 190); } /** * 启动雷电特效 * * @param {HTMLCanvasElement} canvas 全屏 Canvas * @param {Function} onEnd 特效结束回调 */ function start(canvas, onEnd) { const ctx = canvas.getContext("2d"); const FLASHES = 9; const DURATION = 7600; let count = 0; let finished = false; /** * 统一结束特效,避免多次触发 onEnd。 */ function finish() { if (finished) { return; } finished = true; ctx.clearRect(0, 0, canvas.width, canvas.height); onEnd(); } // 间隔不规则触发多次闪电(模拟真实雷电节奏) function nextFlash() { if (count >= FLASHES) { setTimeout(() => { finish(); }, 520); return; } _flash(canvas, ctx); count++; // 让雷电节奏有“成组爆发”的感觉:有时连续两下,有时间隔更久。 const delay = Math.random() > 0.65 ? 140 + Math.random() * 140 : 420 + Math.random() * 520; setTimeout(nextFlash, delay); } // 短暂延迟后开始第一次闪电 setTimeout(nextFlash, 300); // 安全兜底:超时强制结束 setTimeout(() => { finish(); }, DURATION + 500); } return { start }; })();