/** * 文件功能:聊天室下雪特效 * * 使用 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.strokeStyle = "#ffffff"; ctx.lineWidth = Math.max(1, r * 0.12); ctx.shadowColor = "rgba(180, 210, 255, 0.9)"; ctx.shadowBlur = 4; ctx.lineCap = "round"; ctx.translate(x, y); ctx.rotate(rot); // 绘制 6 条主臂(每 60° 一条) 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); } /** * 重置雪花位置 * * @param {boolean} initial 初始化时 Y 随机分布全屏 */ reset(initial = false) { this.x = Math.random() * this.w; this.y = initial ? Math.random() * this.h : -20; this.r = Math.random() * 10 + 6; // 主臂长度 6-16px 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; // 六角雪花绘制开销较大,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 }; })();