128 lines
3.8 KiB
JavaScript
128 lines
3.8 KiB
JavaScript
/**
|
|
* 文件功能:聊天室彩带庆典特效
|
|
*
|
|
* 通过大量彩纸碎片与飘带在空中散落、翻转,形成明显的庆典氛围,
|
|
* 适合活动开始、中奖提示和管理员公告等场景。
|
|
*/
|
|
|
|
const ConfettiEffect = (() => {
|
|
class Piece {
|
|
constructor(w, h) {
|
|
this.w = w;
|
|
this.h = h;
|
|
this.reset();
|
|
}
|
|
|
|
/**
|
|
* 初始化彩带碎片参数。
|
|
*/
|
|
reset() {
|
|
this.x = this.w * (0.15 + Math.random() * 0.7);
|
|
this.y = -Math.random() * this.h * 0.2;
|
|
this.vx = Math.random() * 4.4 - 2.2;
|
|
this.vy = Math.random() * 1.6 + 0.8;
|
|
this.gravity = Math.random() * 0.03 + 0.025;
|
|
this.rot = Math.random() * Math.PI * 2;
|
|
this.rotSpeed = (Math.random() - 0.5) * 0.16;
|
|
this.width = Math.random() * 10 + 6;
|
|
this.height = Math.random() * 18 + 8;
|
|
this.wave = Math.random() * Math.PI * 2;
|
|
this.waveSpeed = Math.random() * 0.06 + 0.03;
|
|
this.alpha = Math.random() * 0.2 + 0.75;
|
|
this.color = ["#ef4444", "#f59e0b", "#22c55e", "#3b82f6", "#8b5cf6", "#ec4899"][
|
|
Math.floor(Math.random() * 6)
|
|
];
|
|
this.isRibbon = Math.random() > 0.72;
|
|
}
|
|
|
|
/**
|
|
* 更新碎片运动状态。
|
|
*/
|
|
update() {
|
|
this.wave += this.waveSpeed;
|
|
this.vy += this.gravity;
|
|
this.vx *= 0.995;
|
|
this.x += this.vx + Math.sin(this.wave) * 0.65;
|
|
this.y += this.vy;
|
|
this.rot += this.rotSpeed;
|
|
}
|
|
|
|
/**
|
|
* 判断彩带是否仍在画布内。
|
|
*/
|
|
get alive() {
|
|
return this.y < this.h + 60;
|
|
}
|
|
|
|
/**
|
|
* 绘制单个彩带碎片。
|
|
*
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
*/
|
|
draw(ctx) {
|
|
const scaleX = Math.max(0.18, Math.abs(Math.cos(this.rot)));
|
|
|
|
ctx.save();
|
|
ctx.translate(this.x, this.y);
|
|
ctx.rotate(this.rot);
|
|
ctx.scale(scaleX, 1);
|
|
ctx.globalAlpha = this.alpha;
|
|
ctx.fillStyle = this.color;
|
|
ctx.shadowColor = `${this.color}66`;
|
|
ctx.shadowBlur = 6;
|
|
|
|
if (this.isRibbon) {
|
|
ctx.fillRect(-this.width * 0.2, -this.height / 2, this.width * 0.4, this.height);
|
|
} else {
|
|
ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 启动彩带庆典特效。
|
|
*
|
|
* @param {HTMLCanvasElement} canvas
|
|
* @param {Function} onEnd
|
|
*/
|
|
function start(canvas, onEnd) {
|
|
const ctx = canvas.getContext("2d");
|
|
const w = canvas.width;
|
|
const h = canvas.height;
|
|
const DURATION = 7800;
|
|
let pieces = Array.from({ length: 90 }, () => new Piece(w, h));
|
|
const startTime = performance.now();
|
|
let lastSpawnAt = startTime;
|
|
let animId = null;
|
|
|
|
function animate(now) {
|
|
ctx.clearRect(0, 0, w, h);
|
|
|
|
pieces = pieces.filter((piece) => {
|
|
piece.update();
|
|
piece.draw(ctx);
|
|
return piece.alive;
|
|
});
|
|
|
|
if (now - startTime < DURATION * 0.9 && now - lastSpawnAt >= 120) {
|
|
pieces.push(...Array.from({ length: 10 }, () => new Piece(w, h)));
|
|
lastSpawnAt = now;
|
|
}
|
|
|
|
if (now - startTime < DURATION) {
|
|
animId = requestAnimationFrame(animate);
|
|
} else {
|
|
cancelAnimationFrame(animId);
|
|
ctx.clearRect(0, 0, w, h);
|
|
onEnd();
|
|
}
|
|
}
|
|
|
|
animId = requestAnimationFrame(animate);
|
|
}
|
|
|
|
return { start };
|
|
})();
|