/** * 文件功能:聊天室金币雨特效 * * 使用 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 isMobile = window.matchMedia?.("(max-width: 640px)")?.matches || window.innerWidth <= 640; const densityScale = isMobile ? 0.72 : 1; const COIN_COUNT = Math.round(Math.min(58, Math.max(28, Math.floor(w / 28))) * densityScale); const coins = Array.from({ length: COIN_COUNT }, () => new Coin(w, h)); const startTime = performance.now(); let animId = null; let finished = false; /** * 统一结束金币雨动画,手动取消时只清理不回调。 * * @param {boolean} canceled 是否为手动取消 */ function finish(canceled) { if (finished) { return; } finished = true; if (animId) { cancelAnimationFrame(animId); } ctx.clearRect(0, 0, w, h); if (!canceled) { onEnd(); } } 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 { finish(false); } } animId = requestAnimationFrame(animate); return { cancel() { finish(true); }, }; } return { start }; })(); window.GoldRainEffect = GoldRainEffect;