/** * 文件功能:聊天室特效管理器 * * 统一管理全屏 Canvas 特效的入口、防重入和资源清理。 * 播放期间用户点击屏幕任意位置可立即结束当前全屏特效。 * 使用方式:EffectManager.play('fireworks' | 'rain' | 'lightning' | 'snow' | 'sakura' | 'meteors' | 'gold-rain' | 'hearts' | 'confetti' | 'fireflies') */ const EffectManager = (() => { // 当前正在播放的特效名称(防止同时播放两个特效) let _current = null; // 全屏 Canvas 元素引用 let _canvas = null; // 待播放特效队列,避免多个进场效果互相打断 const _queue = []; // 当前特效播放批次,用于忽略手动停止后的旧回调 let _playToken = 0; // 是否已经绑定本轮点击停止监听 let _clickStopBound = false; // 延迟绑定定时器,避免触发播放的同一次点击立即停止特效 let _clickStopTimer = null; /** * 获取或创建全屏 Canvas 元素 * 属性:fixed 定位,覆盖全屏,播放期间接收点击用于立即停止特效。 */ function _getCanvas() { if (_canvas && document.body.contains(_canvas)) { return _canvas; } const c = document.createElement("canvas"); c.id = "effect-canvas"; c.style.cssText = [ "position:fixed", "top:0", "left:0", "width:100vw", "height:100vh", "z-index:99999", "pointer-events:auto", "cursor:pointer", "touch-action:manipulation", ].join(";"); c.width = window.innerWidth; c.height = window.innerHeight; document.body.appendChild(c); _canvas = c; return c; } /** * 绑定点击屏幕立即停止当前特效的监听。 * * 延后一帧绑定,避免触发特效的同一次点击被误判为结束点击。 */ function _bindClickStop() { if (_clickStopBound) { return; } _clickStopTimer = window.setTimeout(() => { if (!_current || _clickStopBound) { return; } _clickStopBound = true; if (_canvas) { _canvas.addEventListener("pointerdown", _handleStopClick, { capture: true, }); _canvas.addEventListener("mousedown", _handleStopClick, { capture: true, }); } document.addEventListener("pointerdown", _handleStopClick, { capture: true, }); document.addEventListener("mousedown", _handleStopClick, { capture: true, }); }, 120); } /** * 解绑点击停止监听,避免特效结束后影响正常聊天操作。 */ function _unbindClickStop() { if (_clickStopTimer) { window.clearTimeout(_clickStopTimer); _clickStopTimer = null; } if (!_clickStopBound) { return; } if (_canvas) { _canvas.removeEventListener("pointerdown", _handleStopClick, { capture: true, }); _canvas.removeEventListener("mousedown", _handleStopClick, { capture: true, }); } document.removeEventListener("pointerdown", _handleStopClick, { capture: true, }); document.removeEventListener("mousedown", _handleStopClick, { capture: true, }); _clickStopBound = false; } /** * 处理屏幕点击结束特效。 */ function _handleStopClick(event) { if (event) { event.stopPropagation(); } stop(); } /** * 特效结束后清理 Canvas,重置状态,并停止音效。 * * @param {Object} options 清理选项 * @param {boolean} options.playNext 是否继续播放队列中的下一个特效 * @param {number|null} options.token 当前特效播放批次 */ function _cleanup({ playNext = true, token = null } = {}) { if (token !== null && token !== _playToken) { return; } _playToken++; _unbindClickStop(); if (_canvas && document.body.contains(_canvas)) { document.body.removeChild(_canvas); } _canvas = null; _current = null; // 通知音效引擎停止(兜底:正常情况下音效会自行计时结束) if (typeof EffectSounds !== "undefined") { EffectSounds.stop(); } if (playNext && _queue.length > 0) { const nextType = _queue.shift(); if (nextType) { play(nextType); } } } /** * 播放指定特效 * * @param {string} type 特效类型:fireworks / rain / lightning / snow / sakura / meteors / gold-rain / hearts / confetti / fireflies */ function play(type) { // 防重入:同时只允许一个特效 if (_current) { console.log(`[EffectManager] 特效 ${_current} 正在播放,加入队列 ${type}`); _queue.push(type); return; } const canvas = _getCanvas(); _current = type; const token = _playToken; const finishCurrent = () => _cleanup({ token }); _bindClickStop(); // 同步触发对应音效 if (typeof EffectSounds !== "undefined") { EffectSounds.play(type); } switch (type) { case "fireworks": if (typeof FireworksEffect !== "undefined") { FireworksEffect.start(canvas, finishCurrent); } break; case "wedding-fireworks": // 婚礼专属:双倍礼花,粉金浪漫配色,持续 12 秒 if (typeof FireworksEffect !== "undefined") { FireworksEffect.startDouble(canvas, finishCurrent); } break; case "rain": if (typeof RainEffect !== "undefined") { RainEffect.start(canvas, finishCurrent); } break; case "lightning": if (typeof LightningEffect !== "undefined") { LightningEffect.start(canvas, finishCurrent); } break; case "snow": if (typeof SnowEffect !== "undefined") { SnowEffect.start(canvas, finishCurrent); } break; case "sakura": if (typeof SakuraEffect !== "undefined") { SakuraEffect.start(canvas, finishCurrent); } break; case "meteors": if (typeof MeteorsEffect !== "undefined") { MeteorsEffect.start(canvas, finishCurrent); } break; case "gold-rain": if (typeof GoldRainEffect !== "undefined") { GoldRainEffect.start(canvas, finishCurrent); } break; case "hearts": if (typeof HeartsEffect !== "undefined") { HeartsEffect.start(canvas, finishCurrent); } break; case "confetti": if (typeof ConfettiEffect !== "undefined") { ConfettiEffect.start(canvas, finishCurrent); } break; case "fireflies": if (typeof FirefliesEffect !== "undefined") { FirefliesEffect.start(canvas, finishCurrent); } break; default: console.warn(`[EffectManager] 未知特效类型:${type}`); finishCurrent(); } } /** * 用户手动停止当前全屏特效。 * * 会立即移除画布、停止音效,并清空排队中的后续特效。 */ function stop() { if (!_current) { return; } _queue.length = 0; _cleanup({ playNext: false }); } return { play, stop }; })();