Fix: 下雪特效改为真实六角雪花(6主臂+斜向分叉),支持缓慢旋转和左右飘动
This commit is contained in:
+86
-34
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user