132 lines
4.0 KiB
JavaScript
132 lines
4.0 KiB
JavaScript
/**
|
|
* 文件功能:聊天室金币雨特效
|
|
*
|
|
* 使用 Canvas 绘制翻转下落的金币,让金币带着高光与闪烁效果从天而降,
|
|
* 用于活动奖励、红包雨等庆祝场景。
|
|
*/
|
|
|
|
const GoldRainEffect = (() => {
|
|
class Coin {
|
|
constructor(w, h) {
|
|
this.w = w;
|
|
this.h = h;
|
|
this.reset(true);
|
|
}
|
|
|
|
/**
|
|
* 重置金币位置与翻转参数。
|
|
*
|
|
* @param {boolean} initial 是否首次初始化
|
|
*/
|
|
reset(initial = false) {
|
|
this.x = Math.random() * this.w;
|
|
this.y = initial ? Math.random() * this.h : -30 - Math.random() * 160;
|
|
this.radius = Math.random() * 8 + 12;
|
|
this.speedY = Math.random() * 1.2 + 1.2;
|
|
this.speedX = Math.random() * 0.9 - 0.45;
|
|
this.gravity = Math.random() * 0.035 + 0.015;
|
|
this.spin = Math.random() * Math.PI * 2;
|
|
this.spinSpeed = Math.random() * 0.16 + 0.1;
|
|
this.alpha = Math.random() * 0.25 + 0.72;
|
|
this.sparkle = Math.random() * Math.PI * 2;
|
|
}
|
|
|
|
/**
|
|
* 更新金币状态。
|
|
*/
|
|
update() {
|
|
this.speedY = Math.min(this.speedY + this.gravity, 2.8);
|
|
this.y += this.speedY;
|
|
this.x += this.speedX;
|
|
this.spin += this.spinSpeed;
|
|
this.sparkle += 0.08;
|
|
|
|
if (this.y > this.h + 40) {
|
|
this.reset(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 绘制单枚金币。
|
|
*
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
*/
|
|
draw(ctx) {
|
|
const scaleX = Math.max(0.22, Math.abs(Math.cos(this.spin)));
|
|
const glow = 0.28 + Math.max(0, Math.sin(this.sparkle)) * 0.18;
|
|
|
|
ctx.save();
|
|
ctx.translate(this.x, this.y);
|
|
ctx.scale(scaleX, 1);
|
|
ctx.globalAlpha = this.alpha;
|
|
ctx.shadowColor = "rgba(250, 204, 21, 0.55)";
|
|
ctx.shadowBlur = 10;
|
|
|
|
const gradient = ctx.createLinearGradient(0, -this.radius, 0, this.radius);
|
|
gradient.addColorStop(0, "#fef08a");
|
|
gradient.addColorStop(0.45, "#facc15");
|
|
gradient.addColorStop(1, "#ca8a04");
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeStyle = "rgba(161, 98, 7, 0.7)";
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = `rgba(255,255,255,${glow})`;
|
|
ctx.beginPath();
|
|
ctx.arc(-this.radius * 0.28, -this.radius * 0.32, this.radius * 0.26, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
ctx.scale(1 / scaleX, 1);
|
|
ctx.fillStyle = "rgba(120, 53, 15, 0.8)";
|
|
ctx.font = `${Math.max(10, this.radius)}px Arial`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText("¥", 0, 1);
|
|
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 = 8600;
|
|
const COIN_COUNT = Math.min(58, Math.max(28, Math.floor(w / 28)));
|
|
const coins = Array.from({ length: COIN_COUNT }, () => new Coin(w, h));
|
|
const startTime = performance.now();
|
|
let animId = null;
|
|
|
|
function animate(now) {
|
|
ctx.clearRect(0, 0, w, h);
|
|
|
|
coins.forEach((coin) => {
|
|
coin.update();
|
|
coin.draw(ctx);
|
|
});
|
|
|
|
if (now - startTime < DURATION) {
|
|
animId = requestAnimationFrame(animate);
|
|
} else {
|
|
cancelAnimationFrame(animId);
|
|
ctx.clearRect(0, 0, w, h);
|
|
onEnd();
|
|
}
|
|
}
|
|
|
|
animId = requestAnimationFrame(animate);
|
|
}
|
|
|
|
return { start };
|
|
})();
|