Files
chatroom/public/js/effects/snow.js
T

158 lines
4.8 KiB
JavaScript

/**
* 文件功能:聊天室下雪特效
*
* 使用 Canvas 绘制真实六角雪花图案(6条主臂 + 左右分叉)。
* 采用深色描边+白色主体双遍绘制,在任何背景颜色上都清晰可见。
* 特效总时长约 10 秒,结束后自动清理并回调。
*/
const SnowEffect = (() => {
/**
* 在指定位置绘制一朵六角雪花(深色轮廓 + 白色主体)
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x 中心 x
* @param {number} y 中心 y
* @param {number} r 主臂长度
* @param {number} alpha 透明度
* @param {number} rot 旋转角度(弧度)
*/
function _drawFlake(ctx, x, y, r, alpha, rot) {
ctx.save();
ctx.globalAlpha = alpha;
ctx.lineCap = "round";
ctx.translate(x, y);
ctx.rotate(rot);
// 两遍绘制:先深蓝色粗描边,再白色细线覆盖
// 这样在浅蓝、白色等背景上都清晰可辨
const passes = [
{ color: "rgba(30, 60, 140, 0.8)", lw: r * 0.22 + 2.5 }, // 深蓝粗描边
{ color: "rgba(255, 255, 255, 1.0)", lw: Math.max(1, r * 0.11) }, // 白色主体
];
passes.forEach(({ color, lw }) => {
ctx.strokeStyle = color;
ctx.lineWidth = lw;
for (let i = 0; i < 6; i++) {
ctx.save();
ctx.rotate((Math.PI / 3) * i);
// 主臂
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(r, 0);
ctx.stroke();
// 斜向分叉(0.4r 和 0.65r 处各一对)
const branchLen = r * 0.35;
const branchAngle = Math.PI / 4; // 45°
[0.4, 0.65].forEach((pos) => {
const bx = r * pos;
// 上分叉
ctx.beginPath();
ctx.moveTo(bx, 0);
ctx.lineTo(
bx + Math.cos(branchAngle) * branchLen,
Math.sin(branchAngle) * branchLen,
);
ctx.stroke();
// 下分叉
ctx.beginPath();
ctx.moveTo(bx, 0);
ctx.lineTo(
bx + Math.cos(branchAngle) * branchLen,
-Math.sin(branchAngle) * branchLen,
);
ctx.stroke();
});
ctx.restore();
}
});
ctx.restore();
}
// 雪花粒子类
class Flake {
constructor(w, h) {
this.w = w;
this.h = h;
this.reset(true);
}
reset(initial = false) {
this.x = Math.random() * this.w;
this.y = initial ? Math.random() * this.h : -20;
this.r = Math.random() * 10 + 7; // 主臂长度 7-17px(略放大)
this.speed = Math.random() * 1.2 + 0.4;
this.drift = Math.random() * 0.6 - 0.3;
this.alpha = Math.random() * 0.3 + 0.7; // 透明度 0.7-1.0
this.rot = Math.random() * Math.PI * 2;
this.rotSpd = (Math.random() - 0.5) * 0.02;
this.wobble = 0;
this.wobSpd = Math.random() * 0.03 + 0.01;
}
update() {
this.wobble += this.wobSpd;
this.x += Math.sin(this.wobble) * 0.5 + this.drift;
this.y += this.speed;
this.rot += this.rotSpd;
if (this.y > this.h + 20) {
this.reset(false);
}
}
draw(ctx) {
_drawFlake(ctx, this.x, this.y, this.r, this.alpha, this.rot);
}
}
/**
* 启动下雪特效
*
* @param {HTMLCanvasElement} canvas 全屏 Canvas
* @param {Function} onEnd 特效结束回调
*/
function start(canvas, onEnd) {
const ctx = canvas.getContext("2d");
const w = canvas.width;
const h = canvas.height;
const DURATION = 10000;
const FLAKE_COUNT = 80;
const flakes = Array.from(
{ length: FLAKE_COUNT },
() => new Flake(w, h),
);
let animId = null;
const startTime = performance.now();
function animate(now) {
ctx.clearRect(0, 0, w, h);
flakes.forEach((f) => {
f.update();
f.draw(ctx);
});
if (now - startTime < DURATION) {
animId = requestAnimationFrame(animate);
} else {
cancelAnimationFrame(animId);
ctx.clearRect(0, 0, w, h);
onEnd();
}
}
animId = requestAnimationFrame(animate);
}
return { start };
})();