From 9f8b5e752463465b184d350d96c5b4c1ae2c9549 Mon Sep 17 00:00:00 2001 From: lkddi Date: Fri, 24 Apr 2026 23:15:42 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=82=B9=E5=87=BB=E7=BB=93?= =?UTF-8?q?=E6=9D=9F=E5=85=A8=E5=B1=8F=E7=89=B9=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/js/effects/effect-manager.js | 147 ++++++++++++++++++++++++---- 1 file changed, 129 insertions(+), 18 deletions(-) diff --git a/public/js/effects/effect-manager.js b/public/js/effects/effect-manager.js index 7f47e2d..d136575 100644 --- a/public/js/effects/effect-manager.js +++ b/public/js/effects/effect-manager.js @@ -2,6 +2,7 @@ * 文件功能:聊天室特效管理器 * * 统一管理全屏 Canvas 特效的入口、防重入和资源清理。 + * 播放期间用户点击屏幕任意位置可立即结束当前全屏特效。 * 使用方式:EffectManager.play('fireworks' | 'rain' | 'lightning' | 'snow' | 'sakura' | 'meteors' | 'gold-rain' | 'hearts' | 'confetti' | 'fireflies') */ @@ -12,10 +13,16 @@ const EffectManager = (() => { let _canvas = null; // 待播放特效队列,避免多个进场效果互相打断 const _queue = []; + // 当前特效播放批次,用于忽略手动停止后的旧回调 + let _playToken = 0; + // 是否已经绑定本轮点击停止监听 + let _clickStopBound = false; + // 延迟绑定定时器,避免触发播放的同一次点击立即停止特效 + let _clickStopTimer = null; /** * 获取或创建全屏 Canvas 元素 - * 属性:fixed 定位,覆盖全屏,pointer-events:none 不阻止用户交互 + * 属性:fixed 定位,覆盖全屏,播放期间接收点击用于立即停止特效。 */ function _getCanvas() { if (_canvas && document.body.contains(_canvas)) { @@ -30,7 +37,9 @@ const EffectManager = (() => { "width:100vw", "height:100vh", "z-index:99999", - "pointer-events:none", + "pointer-events:auto", + "cursor:pointer", + "touch-action:manipulation", ].join(";"); c.width = window.innerWidth; c.height = window.innerHeight; @@ -40,9 +49,94 @@ const EffectManager = (() => { } /** - * 特效结束后清理 Canvas,重置状态,并停止音效 + * 绑定点击屏幕立即停止当前特效的监听。 + * + * 延后一帧绑定,避免触发特效的同一次点击被误判为结束点击。 */ - function _cleanup() { + 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); } @@ -53,7 +147,7 @@ const EffectManager = (() => { EffectSounds.stop(); } - if (_queue.length > 0) { + if (playNext && _queue.length > 0) { const nextType = _queue.shift(); if (nextType) { play(nextType); @@ -76,6 +170,9 @@ const EffectManager = (() => { const canvas = _getCanvas(); _current = type; + const token = _playToken; + const finishCurrent = () => _cleanup({ token }); + _bindClickStop(); // 同步触发对应音效 if (typeof EffectSounds !== "undefined") { @@ -85,65 +182,79 @@ const EffectManager = (() => { switch (type) { case "fireworks": if (typeof FireworksEffect !== "undefined") { - FireworksEffect.start(canvas, _cleanup); + FireworksEffect.start(canvas, finishCurrent); } break; case "wedding-fireworks": // 婚礼专属:双倍礼花,粉金浪漫配色,持续 12 秒 if (typeof FireworksEffect !== "undefined") { - FireworksEffect.startDouble(canvas, _cleanup); + FireworksEffect.startDouble(canvas, finishCurrent); } break; case "rain": if (typeof RainEffect !== "undefined") { - RainEffect.start(canvas, _cleanup); + RainEffect.start(canvas, finishCurrent); } break; case "lightning": if (typeof LightningEffect !== "undefined") { - LightningEffect.start(canvas, _cleanup); + LightningEffect.start(canvas, finishCurrent); } break; case "snow": if (typeof SnowEffect !== "undefined") { - SnowEffect.start(canvas, _cleanup); + SnowEffect.start(canvas, finishCurrent); } break; case "sakura": if (typeof SakuraEffect !== "undefined") { - SakuraEffect.start(canvas, _cleanup); + SakuraEffect.start(canvas, finishCurrent); } break; case "meteors": if (typeof MeteorsEffect !== "undefined") { - MeteorsEffect.start(canvas, _cleanup); + MeteorsEffect.start(canvas, finishCurrent); } break; case "gold-rain": if (typeof GoldRainEffect !== "undefined") { - GoldRainEffect.start(canvas, _cleanup); + GoldRainEffect.start(canvas, finishCurrent); } break; case "hearts": if (typeof HeartsEffect !== "undefined") { - HeartsEffect.start(canvas, _cleanup); + HeartsEffect.start(canvas, finishCurrent); } break; case "confetti": if (typeof ConfettiEffect !== "undefined") { - ConfettiEffect.start(canvas, _cleanup); + ConfettiEffect.start(canvas, finishCurrent); } break; case "fireflies": if (typeof FirefliesEffect !== "undefined") { - FirefliesEffect.start(canvas, _cleanup); + FirefliesEffect.start(canvas, finishCurrent); } break; default: console.warn(`[EffectManager] 未知特效类型:${type}`); - _cleanup(); + finishCurrent(); } } - return { play }; + /** + * 用户手动停止当前全屏特效。 + * + * 会立即移除画布、停止音效,并清空排队中的后续特效。 + */ + function stop() { + if (!_current) { + return; + } + + _queue.length = 0; + _cleanup({ playNext: false }); + } + + return { play, stop }; })();