优化聊天室特效加载与移动端性能
This commit is contained in:
Vendored
+1
-12
@@ -1,18 +1,7 @@
|
||||
/**
|
||||
* 文件功能:聊天室全屏特效 Vite 入口
|
||||
*
|
||||
* 按原 public script 顺序加载所有全屏特效模块,让生产环境由 Vite 统一压缩、加 hash 并减少请求数。
|
||||
* 首屏仅加载特效管理器,具体特效模块和音效模块由管理器在首次播放时按需加载。
|
||||
*/
|
||||
|
||||
import "./effects/effect-sounds.js";
|
||||
import "./effects/effect-manager.js";
|
||||
import "./effects/fireworks.js";
|
||||
import "./effects/rain.js";
|
||||
import "./effects/lightning.js";
|
||||
import "./effects/snow.js";
|
||||
import "./effects/sakura.js";
|
||||
import "./effects/meteors.js";
|
||||
import "./effects/gold-rain.js";
|
||||
import "./effects/hearts.js";
|
||||
import "./effects/confetti.js";
|
||||
import "./effects/fireflies.js";
|
||||
|
||||
@@ -92,7 +92,10 @@ const ConfettiEffect = (() => {
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
const DURATION = 7800;
|
||||
let pieces = Array.from({ length: 90 }, () => new Piece(w, h));
|
||||
const isMobile = window.matchMedia?.("(max-width: 640px)")?.matches || window.innerWidth <= 640;
|
||||
const initialPieceCount = isMobile ? 56 : 90;
|
||||
const spawnPieceCount = isMobile ? 6 : 10;
|
||||
let pieces = Array.from({ length: initialPieceCount }, () => new Piece(w, h));
|
||||
const startTime = performance.now();
|
||||
let lastSpawnAt = startTime;
|
||||
let animId = null;
|
||||
@@ -129,7 +132,7 @@ const ConfettiEffect = (() => {
|
||||
});
|
||||
|
||||
if (now - startTime < DURATION * 0.9 && now - lastSpawnAt >= 120) {
|
||||
pieces.push(...Array.from({ length: 10 }, () => new Piece(w, h)));
|
||||
pieces.push(...Array.from({ length: spawnPieceCount }, () => new Piece(w, h)));
|
||||
lastSpawnAt = now;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,24 @@
|
||||
*/
|
||||
|
||||
const EffectManager = (() => {
|
||||
// 音效模块只在首次播放特效时加载,缓存 Promise 避免重复 import
|
||||
let _soundModulePromise = null;
|
||||
// 各特效模块加载器,使用静态路径让 Vite 拆分为可按需加载的 chunk
|
||||
const EFFECT_MODULES = {
|
||||
fireworks: { key: "fireworks", load: () => import("./fireworks.js") },
|
||||
"wedding-fireworks": { key: "fireworks", load: () => import("./fireworks.js") },
|
||||
rain: { key: "rain", load: () => import("./rain.js") },
|
||||
lightning: { key: "lightning", load: () => import("./lightning.js") },
|
||||
snow: { key: "snow", load: () => import("./snow.js") },
|
||||
sakura: { key: "sakura", load: () => import("./sakura.js") },
|
||||
meteors: { key: "meteors", load: () => import("./meteors.js") },
|
||||
"gold-rain": { key: "gold-rain", load: () => import("./gold-rain.js") },
|
||||
hearts: { key: "hearts", load: () => import("./hearts.js") },
|
||||
confetti: { key: "confetti", load: () => import("./confetti.js") },
|
||||
fireflies: { key: "fireflies", load: () => import("./fireflies.js") },
|
||||
};
|
||||
// 特效模块 Promise 缓存,同类型重复触发时复用同一次加载
|
||||
const _effectModulePromises = new Map();
|
||||
// 当前正在播放的特效名称(防止同时播放两个特效)
|
||||
let _current = null;
|
||||
// 当前特效返回的取消函数,用于手动停止时真正停掉内部 RAF / 定时器
|
||||
@@ -17,6 +35,8 @@ const EffectManager = (() => {
|
||||
const _queue = [];
|
||||
// 队列最多保留 3 个待播特效,避免高频触发后播放过期动画
|
||||
const MAX_QUEUE_LENGTH = 3;
|
||||
// 普通特效排队超过 10 秒后丢弃,避免播放过期动画
|
||||
const QUEUED_EFFECT_TTL = 10000;
|
||||
// 当前特效播放批次,用于忽略手动停止后的旧回调
|
||||
let _playToken = 0;
|
||||
// 是否已经绑定本轮点击停止监听
|
||||
@@ -154,6 +174,16 @@ const EffectManager = (() => {
|
||||
stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面进入后台时停止特效,避免隐藏标签页继续运行 Canvas 和音效。
|
||||
*/
|
||||
function _handleVisibilityChange() {
|
||||
if (document.hidden) {
|
||||
stop();
|
||||
_queue.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 特效结束后清理 Canvas,重置状态,并停止音效。
|
||||
*
|
||||
@@ -186,8 +216,8 @@ const EffectManager = (() => {
|
||||
window.EffectSounds.stop();
|
||||
}
|
||||
|
||||
if (playNext && _queue.length > 0) {
|
||||
const nextType = _queue.shift();
|
||||
if (playNext) {
|
||||
const nextType = _dequeueNextType();
|
||||
if (nextType) {
|
||||
play(nextType);
|
||||
}
|
||||
@@ -200,17 +230,84 @@ const EffectManager = (() => {
|
||||
* @param {string} type 待播放特效类型
|
||||
*/
|
||||
function _enqueue(type) {
|
||||
const existingIndex = _queue.indexOf(type);
|
||||
const existingIndex = _queue.findIndex((item) => item.type === type);
|
||||
if (existingIndex !== -1) {
|
||||
_queue.splice(existingIndex, 1);
|
||||
}
|
||||
|
||||
_queue.push(type);
|
||||
_queue.push({
|
||||
type,
|
||||
queuedAt: Date.now(),
|
||||
keepUntilPlayed: type === "wedding-fireworks",
|
||||
});
|
||||
while (_queue.length > MAX_QUEUE_LENGTH) {
|
||||
_queue.shift();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取出下一个仍然有效的排队特效。
|
||||
*
|
||||
* @returns {string|null}
|
||||
*/
|
||||
function _dequeueNextType() {
|
||||
const now = Date.now();
|
||||
|
||||
while (_queue.length > 0) {
|
||||
const next = _queue.shift();
|
||||
if (next.keepUntilPlayed || now - next.queuedAt <= QUEUED_EFFECT_TTL) {
|
||||
return next.type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保音效模块已经加载。
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function _loadSoundModule() {
|
||||
if (!_soundModulePromise) {
|
||||
_soundModulePromise = import("./effect-sounds.js");
|
||||
}
|
||||
|
||||
return _soundModulePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保指定特效模块已经加载。
|
||||
*
|
||||
* @param {string} type 特效类型
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function _loadEffectModule(type) {
|
||||
const moduleConfig = EFFECT_MODULES[type];
|
||||
if (!moduleConfig) {
|
||||
return Promise.reject(new Error(`未知特效类型:${type}`));
|
||||
}
|
||||
|
||||
if (!_effectModulePromises.has(moduleConfig.key)) {
|
||||
_effectModulePromises.set(moduleConfig.key, moduleConfig.load());
|
||||
}
|
||||
|
||||
return _effectModulePromises.get(moduleConfig.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放前加载对应特效模块和音效模块。
|
||||
*
|
||||
* @param {string} type 特效类型
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function _loadModulesFor(type) {
|
||||
return Promise.all([
|
||||
_loadEffectModule(type),
|
||||
_loadSoundModule(),
|
||||
]).then(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录具体特效返回的取消句柄。
|
||||
*
|
||||
@@ -246,16 +343,51 @@ const EffectManager = (() => {
|
||||
* @param {string} type 特效类型:fireworks / rain / lightning / snow / sakura / meteors / gold-rain / hearts / confetti / fireflies
|
||||
*/
|
||||
function play(type) {
|
||||
if (document.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EFFECT_MODULES[type]) {
|
||||
console.warn(`[EffectManager] 未知特效类型:${type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 防重入:同时只允许一个特效
|
||||
if (_current) {
|
||||
console.log(`[EffectManager] 特效 ${_current} 正在播放,加入队列 ${type}`);
|
||||
_enqueue(type);
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = _getCanvas();
|
||||
_play(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载模块后播放指定特效。
|
||||
*
|
||||
* @param {string} type 特效类型
|
||||
*/
|
||||
async function _play(type) {
|
||||
_current = type;
|
||||
const token = _playToken;
|
||||
|
||||
try {
|
||||
await _loadModulesFor(type);
|
||||
} catch (error) {
|
||||
console.error(`[EffectManager] 加载特效模块失败:${type}`, error);
|
||||
_cleanup({ token });
|
||||
return;
|
||||
}
|
||||
|
||||
if (token !== _playToken || _current !== type) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.hidden) {
|
||||
_cleanup({ playNext: false, token });
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = _getCanvas();
|
||||
const finishCurrent = () => _cleanup({ token });
|
||||
_bindClickStop();
|
||||
|
||||
@@ -328,6 +460,8 @@ const EffectManager = (() => {
|
||||
_cleanup({ playNext: false, cancelCurrent: true });
|
||||
}
|
||||
|
||||
document.addEventListener("visibilitychange", _handleVisibilityChange);
|
||||
|
||||
return { play, stop };
|
||||
})();
|
||||
|
||||
|
||||
@@ -102,7 +102,9 @@ const GoldRainEffect = (() => {
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
const DURATION = 8600;
|
||||
const COIN_COUNT = Math.min(58, Math.max(28, Math.floor(w / 28)));
|
||||
const isMobile = window.matchMedia?.("(max-width: 640px)")?.matches || window.innerWidth <= 640;
|
||||
const densityScale = isMobile ? 0.72 : 1;
|
||||
const COIN_COUNT = Math.round(Math.min(58, Math.max(28, Math.floor(w / 28))) * densityScale);
|
||||
const coins = Array.from({ length: COIN_COUNT }, () => new Coin(w, h));
|
||||
const startTime = performance.now();
|
||||
let animId = null;
|
||||
|
||||
@@ -66,7 +66,8 @@ const RainEffect = (() => {
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
const DURATION = 8000;
|
||||
const DROP_COUNT = 200; // 增加雨滴数量(原 180)
|
||||
const isMobile = window.matchMedia?.("(max-width: 640px)")?.matches || window.innerWidth <= 640;
|
||||
const DROP_COUNT = isMobile ? 130 : 200; // 移动端降低雨线数量,避免满屏线段拖慢滚动和输入。
|
||||
|
||||
const drops = Array.from({ length: DROP_COUNT }, () => {
|
||||
const d = new Drop(w, h);
|
||||
|
||||
@@ -161,18 +161,22 @@ const SnowEffect = (() => {
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
const DURATION = 10000;
|
||||
const isMobile = window.matchMedia?.("(max-width: 640px)")?.matches || window.innerWidth <= 640;
|
||||
const densityScale = isMobile ? 0.68 : 1;
|
||||
const backFlakeCount = Math.round(Math.min(120, Math.max(70, Math.floor(w / 18))) * densityScale);
|
||||
const frontFlakeCount = Math.round(Math.min(64, Math.max(34, Math.floor(w / 42))) * densityScale);
|
||||
const flakes = [
|
||||
...Array.from(
|
||||
{ length: Math.min(120, Math.max(70, Math.floor(w / 18))) },
|
||||
{ length: backFlakeCount },
|
||||
() => new Flake(w, h, "back"),
|
||||
),
|
||||
...Array.from(
|
||||
{ length: Math.min(64, Math.max(34, Math.floor(w / 42))) },
|
||||
{ length: frontFlakeCount },
|
||||
() => new Flake(w, h, "front"),
|
||||
),
|
||||
];
|
||||
|
||||
const breezeBands = Array.from({ length: 2 }, () => ({
|
||||
const breezeBands = Array.from({ length: isMobile ? 1 : 2 }, () => ({
|
||||
x: Math.random() * w,
|
||||
y: Math.random() * h,
|
||||
radius: 180 + Math.random() * 140,
|
||||
|
||||
Reference in New Issue
Block a user