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

158 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 文件功能:聊天室下雪特效
*
* 使用 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 };
})();