193 lines
6.3 KiB
JavaScript
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 };
|
||
})();
|