/** * 文件功能:聊天室爱心飘落特效 * * 在透明 Canvas 上绘制成组心形,从上方向下飘落并轻微摇摆, * 适用于表白、婚礼、节日祝福等氛围场景。 */ const HeartsEffect = (() => { class Heart { 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 : -24; this.size = Math.random() * 10 + 10; this.speedY = Math.random() * 1.5 + 1.1; this.swing = Math.random() * Math.PI * 2; this.swingSpeed = Math.random() * 0.04 + 0.015; this.rotation = (Math.random() - 0.5) * 0.35; this.rotationSpeed = (Math.random() - 0.5) * 0.01; this.alpha = Math.random() * 0.28 + 0.62; this.color = ["#fb7185", "#f43f5e", "#ec4899", "#fda4af"][ Math.floor(Math.random() * 4) ]; } /** * 更新爱心位置。 */ update() { this.swing += this.swingSpeed; this.rotation += this.rotationSpeed; this.x += Math.sin(this.swing) * 1.1; this.y += this.speedY; if (this.y > this.h + 28) { this.reset(false); } } /** * 绘制爱心形状。 * * @param {CanvasRenderingContext2D} ctx */ draw(ctx) { const s = this.size; ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.rotation); ctx.scale(s / 16, s / 16); ctx.globalAlpha = this.alpha; ctx.fillStyle = this.color; ctx.shadowColor = "rgba(244, 63, 94, 0.45)"; ctx.shadowBlur = 10; ctx.beginPath(); ctx.moveTo(0, -4); ctx.bezierCurveTo(12, -18, 26, -2, 0, 18); ctx.bezierCurveTo(-26, -2, -12, -18, 0, -4); ctx.fill(); 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 = 9000; const HEART_COUNT = Math.min(48, Math.max(24, Math.floor(w / 34))); const hearts = Array.from({ length: HEART_COUNT }, () => new Heart(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); hearts.forEach((heart) => { heart.update(); heart.draw(ctx); }); if (now - startTime < DURATION) { animId = requestAnimationFrame(animate); } else { finish(false); } } animId = requestAnimationFrame(animate); return { cancel() { finish(true); }, }; } return { start }; })(); window.HeartsEffect = HeartsEffect;