收口聊天室安全边界并优化特效生命周期
This commit is contained in:
@@ -9,16 +9,33 @@
|
||||
const EffectManager = (() => {
|
||||
// 当前正在播放的特效名称(防止同时播放两个特效)
|
||||
let _current = null;
|
||||
// 当前特效返回的取消函数,用于手动停止时真正停掉内部 RAF / 定时器
|
||||
let _currentCancel = null;
|
||||
// 全屏 Canvas 元素引用
|
||||
let _canvas = null;
|
||||
// 待播放特效队列,避免多个进场效果互相打断
|
||||
const _queue = [];
|
||||
// 队列最多保留 3 个待播特效,避免高频触发后播放过期动画
|
||||
const MAX_QUEUE_LENGTH = 3;
|
||||
// 当前特效播放批次,用于忽略手动停止后的旧回调
|
||||
let _playToken = 0;
|
||||
// 是否已经绑定本轮点击停止监听
|
||||
let _clickStopBound = false;
|
||||
// 延迟绑定定时器,避免触发播放的同一次点击立即停止特效
|
||||
let _clickStopTimer = null;
|
||||
// 当前画布像素倍率固定为 1,避免高清手机放大绘制面积拖慢特效
|
||||
const MAX_DPR = 1;
|
||||
|
||||
/**
|
||||
* 按窗口尺寸重置画布像素尺寸。
|
||||
*
|
||||
* @param {HTMLCanvasElement} canvas 全屏特效画布
|
||||
*/
|
||||
function _resizeCanvas(canvas) {
|
||||
const ratio = Math.min(window.devicePixelRatio || 1, MAX_DPR);
|
||||
canvas.width = Math.floor(window.innerWidth * ratio);
|
||||
canvas.height = Math.floor(window.innerHeight * ratio);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建全屏 Canvas 元素
|
||||
@@ -41,13 +58,28 @@ const EffectManager = (() => {
|
||||
"cursor:pointer",
|
||||
"touch-action:manipulation",
|
||||
].join(";");
|
||||
c.width = window.innerWidth;
|
||||
c.height = window.innerHeight;
|
||||
_resizeCanvas(c);
|
||||
document.body.appendChild(c);
|
||||
_canvas = c;
|
||||
window.addEventListener("resize", _handleResize);
|
||||
window.addEventListener("orientationchange", _handleResize);
|
||||
return c;
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应窗口尺寸变化,确保手机横竖屏切换后覆盖范围正确。
|
||||
*/
|
||||
function _handleResize() {
|
||||
if (_current) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_canvas) {
|
||||
_resizeCanvas(_canvas);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定点击屏幕立即停止当前特效的监听。
|
||||
*
|
||||
@@ -129,7 +161,7 @@ const EffectManager = (() => {
|
||||
* @param {boolean} options.playNext 是否继续播放队列中的下一个特效
|
||||
* @param {number|null} options.token 当前特效播放批次
|
||||
*/
|
||||
function _cleanup({ playNext = true, token = null } = {}) {
|
||||
function _cleanup({ playNext = true, token = null, cancelCurrent = false } = {}) {
|
||||
if (token !== null && token !== _playToken) {
|
||||
return;
|
||||
}
|
||||
@@ -137,9 +169,16 @@ const EffectManager = (() => {
|
||||
_playToken++;
|
||||
_unbindClickStop();
|
||||
|
||||
if (cancelCurrent && typeof _currentCancel === "function") {
|
||||
_currentCancel();
|
||||
}
|
||||
_currentCancel = null;
|
||||
|
||||
if (_canvas && document.body.contains(_canvas)) {
|
||||
document.body.removeChild(_canvas);
|
||||
}
|
||||
window.removeEventListener("resize", _handleResize);
|
||||
window.removeEventListener("orientationchange", _handleResize);
|
||||
_canvas = null;
|
||||
_current = null;
|
||||
// 通知音效引擎停止(兜底:正常情况下音效会自行计时结束)
|
||||
@@ -155,6 +194,52 @@ const EffectManager = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将特效加入有限队列,同类型短时间重复触发时只保留一份。
|
||||
*
|
||||
* @param {string} type 待播放特效类型
|
||||
*/
|
||||
function _enqueue(type) {
|
||||
const existingIndex = _queue.indexOf(type);
|
||||
if (existingIndex !== -1) {
|
||||
_queue.splice(existingIndex, 1);
|
||||
}
|
||||
|
||||
_queue.push(type);
|
||||
while (_queue.length > MAX_QUEUE_LENGTH) {
|
||||
_queue.shift();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录具体特效返回的取消句柄。
|
||||
*
|
||||
* @param {Object|undefined} controller 特效启动返回值
|
||||
*/
|
||||
function _bindEffectController(controller) {
|
||||
_currentCancel = typeof controller?.cancel === "function"
|
||||
? controller.cancel
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动具体特效并保存取消句柄。
|
||||
*
|
||||
* @param {Object|undefined} effectObject 特效全局对象
|
||||
* @param {HTMLCanvasElement} canvas 全屏特效画布
|
||||
* @param {Function} finishCurrent 当前特效结束回调
|
||||
* @param {string} startMethod 启动方法名称
|
||||
* @returns {boolean} 是否成功找到并启动特效
|
||||
*/
|
||||
function _startEffect(effectObject, canvas, finishCurrent, startMethod = "start") {
|
||||
if (!effectObject || typeof effectObject[startMethod] !== "function") {
|
||||
return false;
|
||||
}
|
||||
|
||||
_bindEffectController(effectObject[startMethod](canvas, finishCurrent));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放指定特效
|
||||
*
|
||||
@@ -164,7 +249,7 @@ const EffectManager = (() => {
|
||||
// 防重入:同时只允许一个特效
|
||||
if (_current) {
|
||||
console.log(`[EffectManager] 特效 ${_current} 正在播放,加入队列 ${type}`);
|
||||
_queue.push(type);
|
||||
_enqueue(type);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -179,66 +264,53 @@ const EffectManager = (() => {
|
||||
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();
|
||||
let started = false;
|
||||
|
||||
try {
|
||||
switch (type) {
|
||||
case "fireworks":
|
||||
started = _startEffect(typeof FireworksEffect !== "undefined" ? FireworksEffect : undefined, canvas, finishCurrent);
|
||||
break;
|
||||
case "wedding-fireworks":
|
||||
// 婚礼专属:双倍礼花,粉金浪漫配色,持续 12 秒
|
||||
started = _startEffect(typeof FireworksEffect !== "undefined" ? FireworksEffect : undefined, canvas, finishCurrent, "startDouble");
|
||||
break;
|
||||
case "rain":
|
||||
started = _startEffect(typeof RainEffect !== "undefined" ? RainEffect : undefined, canvas, finishCurrent);
|
||||
break;
|
||||
case "lightning":
|
||||
started = _startEffect(typeof LightningEffect !== "undefined" ? LightningEffect : undefined, canvas, finishCurrent);
|
||||
break;
|
||||
case "snow":
|
||||
started = _startEffect(typeof SnowEffect !== "undefined" ? SnowEffect : undefined, canvas, finishCurrent);
|
||||
break;
|
||||
case "sakura":
|
||||
started = _startEffect(typeof SakuraEffect !== "undefined" ? SakuraEffect : undefined, canvas, finishCurrent);
|
||||
break;
|
||||
case "meteors":
|
||||
started = _startEffect(typeof MeteorsEffect !== "undefined" ? MeteorsEffect : undefined, canvas, finishCurrent);
|
||||
break;
|
||||
case "gold-rain":
|
||||
started = _startEffect(typeof GoldRainEffect !== "undefined" ? GoldRainEffect : undefined, canvas, finishCurrent);
|
||||
break;
|
||||
case "hearts":
|
||||
started = _startEffect(typeof HeartsEffect !== "undefined" ? HeartsEffect : undefined, canvas, finishCurrent);
|
||||
break;
|
||||
case "confetti":
|
||||
started = _startEffect(typeof ConfettiEffect !== "undefined" ? ConfettiEffect : undefined, canvas, finishCurrent);
|
||||
break;
|
||||
case "fireflies":
|
||||
started = _startEffect(typeof FirefliesEffect !== "undefined" ? FirefliesEffect : undefined, canvas, finishCurrent);
|
||||
break;
|
||||
default:
|
||||
console.warn(`[EffectManager] 未知特效类型:${type}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[EffectManager] 启动特效失败:${type}`, error);
|
||||
}
|
||||
|
||||
if (!started) {
|
||||
finishCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,8 +325,10 @@ const EffectManager = (() => {
|
||||
}
|
||||
|
||||
_queue.length = 0;
|
||||
_cleanup({ playNext: false });
|
||||
_cleanup({ playNext: false, cancelCurrent: true });
|
||||
}
|
||||
|
||||
return { play, stop };
|
||||
})();
|
||||
|
||||
window.EffectManager = EffectManager;
|
||||
|
||||
Reference in New Issue
Block a user