支持点击结束全屏特效

This commit is contained in:
2026-04-24 23:15:42 +08:00
parent 5273b4ee4b
commit 9f8b5e7524
+129 -18
View File
@@ -2,6 +2,7 @@
* 文件功能:聊天室特效管理器 * 文件功能:聊天室特效管理器
* *
* 统一管理全屏 Canvas 特效的入口、防重入和资源清理。 * 统一管理全屏 Canvas 特效的入口、防重入和资源清理。
* 播放期间用户点击屏幕任意位置可立即结束当前全屏特效。
* 使用方式:EffectManager.play('fireworks' | 'rain' | 'lightning' | 'snow' | 'sakura' | 'meteors' | 'gold-rain' | 'hearts' | 'confetti' | 'fireflies') * 使用方式:EffectManager.play('fireworks' | 'rain' | 'lightning' | 'snow' | 'sakura' | 'meteors' | 'gold-rain' | 'hearts' | 'confetti' | 'fireflies')
*/ */
@@ -12,10 +13,16 @@ const EffectManager = (() => {
let _canvas = null; let _canvas = null;
// 待播放特效队列,避免多个进场效果互相打断 // 待播放特效队列,避免多个进场效果互相打断
const _queue = []; const _queue = [];
// 当前特效播放批次,用于忽略手动停止后的旧回调
let _playToken = 0;
// 是否已经绑定本轮点击停止监听
let _clickStopBound = false;
// 延迟绑定定时器,避免触发播放的同一次点击立即停止特效
let _clickStopTimer = null;
/** /**
* 获取或创建全屏 Canvas 元素 * 获取或创建全屏 Canvas 元素
* 属性:fixed 定位,覆盖全屏,pointer-events:none 不阻止用户交互 * 属性:fixed 定位,覆盖全屏,播放期间接收点击用于立即停止特效。
*/ */
function _getCanvas() { function _getCanvas() {
if (_canvas && document.body.contains(_canvas)) { if (_canvas && document.body.contains(_canvas)) {
@@ -30,7 +37,9 @@ const EffectManager = (() => {
"width:100vw", "width:100vw",
"height:100vh", "height:100vh",
"z-index:99999", "z-index:99999",
"pointer-events:none", "pointer-events:auto",
"cursor:pointer",
"touch-action:manipulation",
].join(";"); ].join(";");
c.width = window.innerWidth; c.width = window.innerWidth;
c.height = window.innerHeight; 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)) { if (_canvas && document.body.contains(_canvas)) {
document.body.removeChild(_canvas); document.body.removeChild(_canvas);
} }
@@ -53,7 +147,7 @@ const EffectManager = (() => {
EffectSounds.stop(); EffectSounds.stop();
} }
if (_queue.length > 0) { if (playNext && _queue.length > 0) {
const nextType = _queue.shift(); const nextType = _queue.shift();
if (nextType) { if (nextType) {
play(nextType); play(nextType);
@@ -76,6 +170,9 @@ const EffectManager = (() => {
const canvas = _getCanvas(); const canvas = _getCanvas();
_current = type; _current = type;
const token = _playToken;
const finishCurrent = () => _cleanup({ token });
_bindClickStop();
// 同步触发对应音效 // 同步触发对应音效
if (typeof EffectSounds !== "undefined") { if (typeof EffectSounds !== "undefined") {
@@ -85,65 +182,79 @@ const EffectManager = (() => {
switch (type) { switch (type) {
case "fireworks": case "fireworks":
if (typeof FireworksEffect !== "undefined") { if (typeof FireworksEffect !== "undefined") {
FireworksEffect.start(canvas, _cleanup); FireworksEffect.start(canvas, finishCurrent);
} }
break; break;
case "wedding-fireworks": case "wedding-fireworks":
// 婚礼专属:双倍礼花,粉金浪漫配色,持续 12 秒 // 婚礼专属:双倍礼花,粉金浪漫配色,持续 12 秒
if (typeof FireworksEffect !== "undefined") { if (typeof FireworksEffect !== "undefined") {
FireworksEffect.startDouble(canvas, _cleanup); FireworksEffect.startDouble(canvas, finishCurrent);
} }
break; break;
case "rain": case "rain":
if (typeof RainEffect !== "undefined") { if (typeof RainEffect !== "undefined") {
RainEffect.start(canvas, _cleanup); RainEffect.start(canvas, finishCurrent);
} }
break; break;
case "lightning": case "lightning":
if (typeof LightningEffect !== "undefined") { if (typeof LightningEffect !== "undefined") {
LightningEffect.start(canvas, _cleanup); LightningEffect.start(canvas, finishCurrent);
} }
break; break;
case "snow": case "snow":
if (typeof SnowEffect !== "undefined") { if (typeof SnowEffect !== "undefined") {
SnowEffect.start(canvas, _cleanup); SnowEffect.start(canvas, finishCurrent);
} }
break; break;
case "sakura": case "sakura":
if (typeof SakuraEffect !== "undefined") { if (typeof SakuraEffect !== "undefined") {
SakuraEffect.start(canvas, _cleanup); SakuraEffect.start(canvas, finishCurrent);
} }
break; break;
case "meteors": case "meteors":
if (typeof MeteorsEffect !== "undefined") { if (typeof MeteorsEffect !== "undefined") {
MeteorsEffect.start(canvas, _cleanup); MeteorsEffect.start(canvas, finishCurrent);
} }
break; break;
case "gold-rain": case "gold-rain":
if (typeof GoldRainEffect !== "undefined") { if (typeof GoldRainEffect !== "undefined") {
GoldRainEffect.start(canvas, _cleanup); GoldRainEffect.start(canvas, finishCurrent);
} }
break; break;
case "hearts": case "hearts":
if (typeof HeartsEffect !== "undefined") { if (typeof HeartsEffect !== "undefined") {
HeartsEffect.start(canvas, _cleanup); HeartsEffect.start(canvas, finishCurrent);
} }
break; break;
case "confetti": case "confetti":
if (typeof ConfettiEffect !== "undefined") { if (typeof ConfettiEffect !== "undefined") {
ConfettiEffect.start(canvas, _cleanup); ConfettiEffect.start(canvas, finishCurrent);
} }
break; break;
case "fireflies": case "fireflies":
if (typeof FirefliesEffect !== "undefined") { if (typeof FirefliesEffect !== "undefined") {
FirefliesEffect.start(canvas, _cleanup); FirefliesEffect.start(canvas, finishCurrent);
} }
break; break;
default: default:
console.warn(`[EffectManager] 未知特效类型:${type}`); console.warn(`[EffectManager] 未知特效类型:${type}`);
_cleanup(); finishCurrent();
} }
} }
return { play }; /**
* 用户手动停止当前全屏特效。
*
* 会立即移除画布、停止音效,并清空排队中的后续特效。
*/
function stop() {
if (!_current) {
return;
}
_queue.length = 0;
_cleanup({ playNext: false });
}
return { play, stop };
})(); })();