diff --git a/public/js/effects/snow.js b/public/js/effects/snow.js index a279763..a7dd9cf 100644 --- a/public/js/effects/snow.js +++ b/public/js/effects/snow.js @@ -1,13 +1,76 @@ /** * 文件功能:聊天室下雪特效 * - * 使用 Canvas 绘制随机飘落的雪花圆点,模拟冬日飘雪效果。 - * 雪花大小、速度、飘动幅度随机,在浅色背景上以白色+深描边显示。 + * 使用 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; @@ -18,45 +81,35 @@ const SnowEffect = (() => { /** * 重置雪花位置 * - * @param {boolean} initial 是否初始化(初始化时 Y 随机分布全屏,否则从顶部重生) + * @param {boolean} initial 初始化时 Y 随机分布全屏 */ reset(initial = false) { this.x = Math.random() * this.w; - this.y = initial ? Math.random() * this.h : -10; - this.r = Math.random() * 4 + 2; // 半径 2-6 - this.speed = Math.random() * 1.5 + 0.5; // 下落速度 - this.drift = Math.random() * 0.8 - 0.4; // 水平漂移 - this.alpha = Math.random() * 0.4 + 0.6; // 透明度 0.6-1.0 - this.angle = 0; - this.wobble = Math.random() * 0.04 + 0.01; // 左右摇摆频率 + 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.angle += this.wobble; - this.x += Math.sin(this.angle) * this.drift + this.drift * 0.3; + this.wobble += this.wobSpd; + this.x += Math.sin(this.wobble) * 0.5 + this.drift; this.y += this.speed; - if (this.y > this.h + 10) { + this.rot += this.rotSpd; + if (this.y > this.h + 20) { this.reset(false); } } - /** 绘制雪花(白色圆点 + 深色描边,在浅色背景上可见) */ + /** 绘制雪花 */ draw(ctx) { - ctx.save(); - ctx.globalAlpha = this.alpha; - // 外圈:半透明蓝灰描边 - ctx.beginPath(); - ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2); - ctx.strokeStyle = "rgba(80, 120, 180, 0.6)"; - ctx.lineWidth = 0.8; - ctx.stroke(); - // 内部:白色填充 - ctx.fillStyle = "#ffffff"; - ctx.shadowColor = "rgba(150, 180, 255, 0.8)"; - ctx.shadowBlur = 4; - ctx.fill(); - ctx.restore(); + _drawFlake(ctx, this.x, this.y, this.r, this.alpha, this.rot); } } @@ -70,10 +123,10 @@ const SnowEffect = (() => { const ctx = canvas.getContext("2d"); const w = canvas.width; const h = canvas.height; - const DURATION = 10000; // 总时长(ms) - const FLAKE_COUNT = 160; // 雪花数量 + const DURATION = 10000; + const FLAKE_COUNT = 80; // 六角雪花绘制开销较大,80 个足够 - // 初始化雪花,随机分布全屏(避免开始时全堆在顶部) + // 初始化所有雪花,随机分布全屏 const flakes = Array.from( { length: FLAKE_COUNT }, () => new Flake(w, h), @@ -83,7 +136,6 @@ const SnowEffect = (() => { const startTime = performance.now(); function animate(now) { - // 清除画布(透明,不遮挡聊天背景) ctx.clearRect(0, 0, w, h); flakes.forEach((f) => {