Files

193 lines
6.3 KiB
JavaScript

/**
* 文件功能:聊天室雷电特效
*
* 使用递归分叉算法叠加云层闪光、主闪电、余辉残影,
* 在聊天室中模拟更有压迫感的雷暴闪电效果。
*/
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 };
})();