diff --git a/packages/webgal/package.json b/packages/webgal/package.json index a4709300a..3ca325735 100644 --- a/packages/webgal/package.json +++ b/packages/webgal/package.json @@ -16,6 +16,7 @@ "axios": "^1.13.5", "cloudlogjs": "^1.0.9", "gifuct-js": "^2.1.2", + "gsap": "^3.14.2", "i18next": "^22.4.15", "localforage": "^1.10.0", "lodash": "^4.17.23", diff --git a/packages/webgal/src/Core/Modules/audio/bgmManager.ts b/packages/webgal/src/Core/Modules/audio/bgmManager.ts new file mode 100644 index 000000000..24a080cfd --- /dev/null +++ b/packages/webgal/src/Core/Modules/audio/bgmManager.ts @@ -0,0 +1,269 @@ +import { setStage } from '@/store/stageReducer'; +import { webgalStore } from '@/store/store'; +import gsap from 'gsap'; + +class BgmManager { + private static instance: BgmManager; + + public static getInstance(): BgmManager { + if (!BgmManager.instance) { + BgmManager.instance = new BgmManager(); + } + return BgmManager.instance; + } + + private audios: [HTMLAudioElement, HTMLAudioElement]; + private currentIndex = 0; + private src = ''; + private targetVolume = 100; + private progressListeners: Set<(p: { currentTime: number; duration: number }) => void> = new Set(); + + private constructor() { + this.audios = [new Audio(), new Audio()]; + this.audios.forEach((audio) => { + audio.loop = true; + audio.preload = 'auto'; + audio.crossOrigin = 'anonymous'; + audio.addEventListener('timeupdate', this.onTimeUpdate); + }); + } + + /** + * 播放bgm + * @param options.src bgm路径 + * @param options.volume 背景音乐 音量调整(0 - 100) + * @param options.enter 淡入时间(单位毫秒) + * @param options.exit 淡出时间(单位毫秒) + * @param options.loop 是否循环播放 + */ + public async play( + options: { src?: string; volume?: number; enter?: number; exit?: number; loop?: boolean } = {}, + ): Promise { + const src = options.src ?? this.src; + const enter = options.enter ?? 0; + const exit = options.exit ?? enter; + const volume = Math.max(0, Math.min(100, Math.trunc(options.volume ?? this.targetVolume))); + const loop = options.loop ?? this.loop; + + this.targetVolume = volume; + this.loop = loop; + + if (src === '') { + await this.stop(exit); + return; + } + + webgalStore.dispatch(setStage({ key: 'bgm', value: { src, volume, enter, exit } })); + + if (src === this.src) { + if (this.audio.paused) { + this.audio.volume = 0; + await this.audio.play(); + } + await this.setVolume({ audio: this.audio, volume: this.targetVolume, duration: enter }); + return; + } + + const oldIndex = this.currentIndex; + const nextIndex = (this.currentIndex + 1) % 2; + const oldAudio = this.audios[oldIndex]; + const nextAudio = this.audios[nextIndex]; + + nextAudio.pause(); + nextAudio.src = src; + nextAudio.volume = 0; + + try { + nextAudio.load(); + await new Promise((resolve, reject) => { + const cleanup = () => { + nextAudio.removeEventListener('canplaythrough', onResolve); + nextAudio.removeEventListener('error', onReject); + }; + + const onResolve = () => { + cleanup(); + resolve(null); + }; + + const onReject = (e: Event) => { + cleanup(); + reject(e); + }; + nextAudio.addEventListener('canplaythrough', onResolve, { once: true }); + nextAudio.addEventListener('error', onReject, { once: true }); + }); + + await nextAudio.play(); + this.currentIndex = nextIndex; + + if (enter > 0) { + await Promise.all([ + this.setVolume({ audio: oldAudio, volume: 0, duration: exit, stopOnEnd: true }), + this.setVolume({ audio: nextAudio, volume: this.targetVolume, duration: enter }), + ]); + } else { + this.resetAudio(oldAudio); + } + + this.src = src; + } catch (e) { + console.error('BGM Playback failed:', e); + this.resetAudio(nextAudio); + } + } + + public async pause(value = 0): Promise { + if (value > 0) { + await this.setVolume({ audio: this.audio, volume: 0, duration: value, pauseOnEnd: true }); + } else { + this.audio.pause(); + } + } + + public async stop(value = 0): Promise { + this.src = ''; + this.targetVolume = 100; + webgalStore.dispatch(setStage({ key: 'bgm', value: { src: '', volume: 100, enter: 0, exit: 0 } })); + if (value > 0) { + await this.setVolume({ audio: this.audio, volume: 0, duration: value, stopOnEnd: true }); + } else { + this.audios.forEach((_, i) => this.resetAudio(this.audios[i])); + } + } + + public async resume(value = 0): Promise { + return this.play({ enter: value }); + } + + public refreshVolume() { + this.volume = this.targetVolume; + } + + public addProgressListener(cb: (p: { currentTime: number; duration: number }) => void): () => void { + this.progressListeners.add(cb); + + return () => { + this.progressListeners.delete(cb); + }; + } + + public clearListeners(): void { + this.progressListeners.clear(); + } + + private get audio() { + return this.audios[this.currentIndex]; + } + + public get currentTime() { + return this.audio.currentTime; + } + public set currentTime(value: number) { + this.audio.currentTime = value; + } + + public get duration() { + return this.audio.duration; + } + public get paused() { + return this.audio.paused; + } + + public get volume() { + return this.targetVolume; + } + public set volume(value: number) { + const volume = Math.max(0, Math.min(100, Math.trunc(value))); + this.targetVolume = volume; + + const computedVolume = this.getComputedVolume(volume); + + const activeTweens = gsap.getTweensOf(this.audio, true); + if (activeTweens.length > 0) { + activeTweens.forEach((tween) => { + tween.vars.volume = computedVolume; + tween.invalidate(); + }); + } else { + this.audio.volume = computedVolume; + } + } + + public get loop() { + return this.audio.loop; + } + public set loop(value: boolean) { + this.audios.forEach((a) => { + a.loop = value; + }); + } + + public getComputedVolume(value?: number): number { + const { userData, stage } = webgalStore.getState(); + const { optionData } = userData; + + const main = optionData.volumeMain * 0.01; + const group = optionData.bgmVolume * 0.01; + const current = (value ?? stage.bgm.volume) * 0.01; + + return main * group * current; + } + + private setVolume(options: { + audio: HTMLAudioElement; + volume: number; + duration: number; + stopOnEnd?: boolean; + pauseOnEnd?: boolean; + }): Promise { + const { audio, volume, duration, stopOnEnd, pauseOnEnd } = options; + + if (!audio.src || audio.src === '' || audio.src === window.location.href) { + return Promise.resolve(); + } + + return new Promise((resolve) => { + const computedVolume = this.getComputedVolume(volume); + const vars: gsap.TweenVars = { + volume: computedVolume, + duration: duration / 1000, + ease: computedVolume > audio.volume ? 'sine.out' : 'sine.in', + overwrite: 'auto', + onComplete: () => { + if (stopOnEnd) this.resetAudio(audio); + else if (pauseOnEnd) audio.pause(); + resolve(); + }, + onInterrupt: () => resolve(), + }; + + if (duration <= 0) { + gsap.set(audio, vars); + } else { + gsap.to(audio, vars); + } + }); + } + + private onTimeUpdate = () => { + if (this.src === '' || this.progressListeners.size === 0) return; + const { currentTime, duration } = this.audio; + this.progressListeners.forEach((listener) => listener({ currentTime, duration })); + }; + + private resetAudio(audio: HTMLAudioElement) { + gsap.killTweensOf(audio); + + audio.pause(); + audio.volume = 0; + audio.loop = true; + + audio.removeAttribute('src'); + audio.load(); + } +} + +const bgmManager = BgmManager.getInstance(); + +export default bgmManager; diff --git a/packages/webgal/src/Core/controller/stage/playBgm.ts b/packages/webgal/src/Core/controller/stage/playBgm.ts deleted file mode 100644 index b6e76c2e8..000000000 --- a/packages/webgal/src/Core/controller/stage/playBgm.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { webgalStore } from '@/store/store'; -import { setStage } from '@/store/stageReducer'; -import { logger } from '@/Core/util/logger'; - -// /** -// * 停止bgm -// */ -// export const eraseBgm = () => { -// logger.debug(`停止bgm`); -// // 停止之前的bgm -// let VocalControl: any = document.getElementById('currentBgm'); -// if (VocalControl !== null) { -// VocalControl.currentTime = 0; -// if (!VocalControl.paused) VocalControl.pause(); -// } -// // 获得舞台状态并设置 -// webgalStore.dispatch(setStage({key: 'bgm', value: ''})); -// }; - -let emptyBgmTimeout: ReturnType; - -/** - * 播放bgm - * @param url bgm路径 - * @param enter 淡入时间(单位毫秒) - * @param volume 背景音乐 音量调整(0 - 100) - */ -export function playBgm(url: string, enter = 0, volume = 100): void { - logger.debug('playing bgm' + url); - if (url === '') { - emptyBgmTimeout = setTimeout(() => { - // 淡入淡出效果结束后,将 bgm 置空 - webgalStore.dispatch(setStage({ key: 'bgm', value: { src: '', enter: 0, volume: 100 } })); - }, enter); - const lastSrc = webgalStore.getState().stage.bgm.src; - webgalStore.dispatch(setStage({ key: 'bgm', value: { src: lastSrc, enter: -enter, volume: volume } })); - } else { - // 不要清除bgm了! - clearTimeout(emptyBgmTimeout); - webgalStore.dispatch(setStage({ key: 'bgm', value: { src: url, enter: enter, volume: volume } })); - } - setTimeout(() => { - const audioElement = document.getElementById('currentBgm') as HTMLAudioElement; - if (audioElement.src) { - audioElement?.play(); - } - }, 0); -} diff --git a/packages/webgal/src/Core/controller/stage/setVolume.ts b/packages/webgal/src/Core/controller/stage/setVolume.ts deleted file mode 100644 index 14f0ec36f..000000000 --- a/packages/webgal/src/Core/controller/stage/setVolume.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { logger } from '../../util/logger'; -import { webgalStore } from '@/store/store'; - -/** - * 设置音量 - */ -export const setVolume = () => { - const userDataState = webgalStore.getState().userData; - const mainVol = userDataState.optionData.volumeMain; - const vocalVol = mainVol * 0.01 * userDataState.optionData.vocalVolume * 0.01; - const bgmVol = mainVol * 0.01 * userDataState.optionData.bgmVolume * 0.01; - logger.debug(`设置背景音量:${bgmVol},语音音量:${vocalVol}`); - // const bgmElement: any = document.getElementById('currentBgm'); - // if (bgmElement) { - // bgmElement.volume = bgmVol.toString(); - // } - // const vocalElement: any = document.getElementById('currentVocal'); - // if (vocalElement) { - // vocalElement.volume = vocalVol.toString(); - // } -}; diff --git a/packages/webgal/src/Core/gameScripts/bgm.ts b/packages/webgal/src/Core/gameScripts/bgm.ts index d39a88ed7..6ebd34682 100644 --- a/packages/webgal/src/Core/gameScripts/bgm.ts +++ b/packages/webgal/src/Core/gameScripts/bgm.ts @@ -1,11 +1,11 @@ import { ISentence } from '@/Core/controller/scene/sceneInterface'; import { IPerform } from '@/Core/Modules/perform/performInterface'; -import { playBgm } from '@/Core/controller/stage/playBgm'; import { getNumberArgByKey, getStringArgByKey } from '@/Core/util/getSentenceArg'; import { webgalStore } from '@/store/store'; import { unlockBgmInUserData } from '@/store/userDataReducer'; import localforage from 'localforage'; import { WebGAL } from '../WebGAL'; +import bgmManager from '../Modules/audio/bgmManager'; /** * 播放一段bgm @@ -15,24 +15,26 @@ export const bgm = (sentence: ISentence): IPerform => { let url: string = sentence.content; // 获取bgm的url const name = getStringArgByKey(sentence, 'unlockname') ?? ''; const series = getStringArgByKey(sentence, 'series') ?? 'default'; - let enter = getNumberArgByKey(sentence, 'enter') ?? 0; // 获取bgm的淡入时间 - enter = Math.max(0, enter); // 限制淡入时间在 0 以上 let volume = getNumberArgByKey(sentence, 'volume') ?? 100; // 获取bgm的音量比 volume = Math.max(0, Math.min(volume, 100)); // 限制音量在 0-100 之间 + let enter = getNumberArgByKey(sentence, 'enter') ?? 0; // 获取bgm的淡入时间 + enter = Math.max(0, enter); // 限制淡入时间在 0 以上 + let exit = getNumberArgByKey(sentence, 'exit') ?? enter; // 获取bgm的淡出时间 + exit = Math.max(0, exit); // 限制淡出时间在 0 以上 if (name !== '') { webgalStore.dispatch(unlockBgmInUserData({ name, url, series })); const userDataState = webgalStore.getState().userData; - localforage.setItem(WebGAL.gameKey, userDataState).then(() => {}); + localforage.setItem(WebGAL.gameKey, userDataState).then(() => { }); } - playBgm(url, enter, volume); + bgmManager.play({ src: url, volume, enter, exit }); return { performName: 'none', duration: 0, isHoldOn: true, - stopFunction: () => {}, + stopFunction: () => { }, blockingNext: () => false, blockingAuto: () => true, stopTimeout: undefined, // 暂时不用,后面会交给自动清除 diff --git a/packages/webgal/src/Core/gameScripts/end.ts b/packages/webgal/src/Core/gameScripts/end.ts index 3902472bf..02fbb628b 100644 --- a/packages/webgal/src/Core/gameScripts/end.ts +++ b/packages/webgal/src/Core/gameScripts/end.ts @@ -6,7 +6,7 @@ import { sceneParser } from '@/Core/parser/sceneParser'; import { resetStage } from '@/Core/controller/stage/resetStage'; import { webgalStore } from '@/store/store'; import { setVisibility } from '@/store/GUIReducer'; -import { playBgm } from '@/Core/controller/stage/playBgm'; +import bgmManager from '@/Core/Modules/audio/bgmManager'; import { WebGAL } from '@/Core/WebGAL'; import { dumpToStorageFast } from '@/Core/controller/storage/storageController'; import { saveActions } from '@/store/savesReducer'; @@ -31,12 +31,12 @@ export const end = (sentence: ISentence): IPerform => { WebGAL.sceneManager.sceneData.currentScene = sceneParser(rawScene, 'start.txt', sceneUrl); }); dispatch(setVisibility({ component: 'showTitle', visibility: true })); - playBgm(webgalStore.getState().GUI.titleBgm); + bgmManager.play({ src: webgalStore.getState().GUI.titleBgm, volume: 100, enter: 2000 }); return { performName: 'none', duration: 0, isHoldOn: false, - stopFunction: () => {}, + stopFunction: () => { }, blockingNext: () => false, blockingAuto: () => true, stopTimeout: undefined, // 暂时不用,后面会交给自动清除 diff --git a/packages/webgal/src/Core/gameScripts/playVideo.tsx b/packages/webgal/src/Core/gameScripts/playVideo.tsx index d3dbd4e69..d3ee0867b 100644 --- a/packages/webgal/src/Core/gameScripts/playVideo.tsx +++ b/packages/webgal/src/Core/gameScripts/playVideo.tsx @@ -7,14 +7,18 @@ import { webgalStore } from '@/store/store'; import { getRandomPerformName, PerformController } from '@/Core/Modules/perform/performController'; import { getBooleanArgByKey } from '@/Core/util/getSentenceArg'; import { WebGAL } from '@/Core/WebGAL'; +import bgmManager from '../Modules/audio/bgmManager'; /** * 播放一段视频 * @param sentence */ export const playVideo = (sentence: ISentence): IPerform => { + const stageState = webgalStore.getState().stage; const userDataState = webgalStore.getState().userData; const mainVol = userDataState.optionData.volumeMain; const vocalVol = mainVol * 0.01 * userDataState.optionData.vocalVolume * 0.01; const bgmVol = mainVol * 0.01 * userDataState.optionData.bgmVolume * 0.01; + const bgmEnter = stageState.bgm.enter; + const bgmExit = stageState.bgm.exit; const performInitName: string = getRandomPerformName(); let blockingNextFlag = getBooleanArgByKey(sentence, 'skipOff') ?? false; @@ -31,7 +35,7 @@ export const playVideo = (sentence: ISentence): IPerform => { performName: 'none', duration: 0, isHoldOn: false, - stopFunction: () => {}, + stopFunction: () => { }, blockingNext: () => blockingNextFlag, blockingAuto: () => true, stopTimeout: undefined, // 暂时不用,后面会交给自动清除 @@ -69,12 +73,9 @@ export const playVideo = (sentence: ISentence): IPerform => { /** * 恢复音量 */ - const bgmElement: any = document.getElementById('currentBgm'); - if (bgmElement) { - bgmElement.volume = bgmVol.toString(); - } + bgmManager.resume(bgmEnter); const vocalElement: any = document.getElementById('currentVocal'); - if (bgmElement) { + if (vocalElement) { vocalElement.volume = vocalVol.toString(); } // eslint-disable-next-line react/no-deprecated @@ -93,12 +94,9 @@ export const playVideo = (sentence: ISentence): IPerform => { */ const vocalVol2 = 0; const bgmVol2 = 0; - const bgmElement: any = document.getElementById('currentBgm'); - if (bgmElement) { - bgmElement.volume = bgmVol2.toString(); - } + bgmManager.pause(bgmExit); const vocalElement: any = document.getElementById('currentVocal'); - if (bgmElement) { + if (vocalElement) { vocalElement.volume = vocalVol2.toString(); } diff --git a/packages/webgal/src/Stage/AudioContainer/AudioContainer.tsx b/packages/webgal/src/Stage/AudioContainer/AudioContainer.tsx index c82e80c92..544457a2d 100644 --- a/packages/webgal/src/Stage/AudioContainer/AudioContainer.tsx +++ b/packages/webgal/src/Stage/AudioContainer/AudioContainer.tsx @@ -3,81 +3,34 @@ import { RootState, webgalStore } from '@/store/store'; import { setStage } from '@/store/stageReducer'; import { useEffect, useState } from 'react'; import { logger } from '@/Core/util/logger'; +import bgmManager from '@/Core/Modules/audio/bgmManager'; export const AudioContainer = () => { const stageStore = useSelector((webgalStore: RootState) => webgalStore.stage); - const titleBgm = useSelector((webgalStore: RootState) => webgalStore.GUI.titleBgm); - const isShowTitle = useSelector((webgalStore: RootState) => webgalStore.GUI.showTitle); const userDataState = useSelector((state: RootState) => state.userData); const mainVol = userDataState.optionData.volumeMain; const vocalBaseVol = mainVol * 0.01 * userDataState.optionData.vocalVolume * 0.01; const vocalVol = vocalBaseVol * stageStore.vocalVolume * 0.01; const bgmVol = mainVol * 0.01 * userDataState.optionData.bgmVolume * 0.01 * stageStore.bgm.volume * 0.01; const bgmEnter = stageStore.bgm.enter; + const bgmExit = stageStore.bgm.exit; const uiSoundEffects = stageStore.uiSe; const seVol = mainVol * 0.01 * (userDataState.optionData?.seVolume ?? 100) * 0.01; const uiSeVol = mainVol * 0.01 * (userDataState.optionData.uiSeVolume ?? 50) * 0.01; - const isEnterGame = useSelector((state: RootState) => state.GUI.isEnterGame); - - // 淡入淡出定时器 - const [fadeTimer, setFadeTimer] = useState(setTimeout(() => {}, 0)); - - /** - * 淡入BGM - * @param bgm 背景音乐 - * @param maxVol 最大音量 - * @param time 淡入时间 - */ - const bgmFadeIn = (bgm: HTMLAudioElement, maxVol: number, time: number) => { - // 设置初始音量 - time >= 0 ? (bgm.volume = 0) : (bgm.volume = maxVol); - // 设置音量递增时间间隔 - const duration = 10; - // 计算每duration的音量增量 - const volumeStep = (maxVol / time) * duration; - // 基于递归调用实现淡入淡出效果 - const fade = () => { - const timer = setTimeout(() => { - if (bgm.volume + volumeStep >= maxVol) { - // 如果音量接近或达到最大值,则设置最终音量(淡入) - bgm.volume = maxVol; - } else if (bgm.volume + volumeStep <= 0) { - // 如果音量接近或达到最小值,则设置最终音量(淡出) - bgm.volume = 0; - // 淡出效果结束后,将 bgm 置空 - webgalStore.dispatch(setStage({ key: 'bgm', value: { src: '', enter: 0, volume: 100 } })); - } else { - // 否则增加音量,并递归调用 - bgm.volume += volumeStep; - fade(); - } - }, duration); - // 将定时器引用存储到 fadeTimer 中 - setFadeTimer(timer); - }; - // 调用淡入淡出函数 - fade(); - }; - - useEffect(() => { - // 清除之前的淡入淡出定时器 - clearTimeout(fadeTimer); - // 获取当前背景音乐元素 - const bgmElement = document.getElementById('currentBgm') as HTMLAudioElement; - // 如果当前背景音乐元素存在,则淡入淡出 - if (bgmElement) { - bgmEnter === 0 ? (bgmElement.volume = bgmVol) : bgmFadeIn(bgmElement, bgmVol, bgmEnter); - } - }, [isShowTitle, titleBgm, stageStore.bgm.src, bgmVol, bgmEnter]); useEffect(() => { logger.debug(`设置背景音量:${bgmVol}`); + bgmManager.refreshVolume(); }, [bgmVol]); useEffect(() => { logger.debug(`设置背景音量淡入时间: ${bgmEnter}`); }, [bgmEnter]); + useEffect(() => { + logger.debug(`设置背景音量淡出时间: ${bgmExit}`); + }, [bgmExit]); + useEffect(() => { logger.debug(`设置语音音量:${vocalVol}`); const vocalElement: any = document.getElementById('currentVocal'); @@ -118,13 +71,6 @@ export const AudioContainer = () => { return (
-
); diff --git a/packages/webgal/src/UI/Extra/ExtraBgm.tsx b/packages/webgal/src/UI/Extra/ExtraBgm.tsx index 7c9bba9b2..c7cca56c2 100644 --- a/packages/webgal/src/UI/Extra/ExtraBgm.tsx +++ b/packages/webgal/src/UI/Extra/ExtraBgm.tsx @@ -1,5 +1,5 @@ import { useDispatch, useSelector } from 'react-redux'; -import { RootState } from '@/store/store'; +import { RootState, webgalStore } from '@/store/store'; import React from 'react'; import styles from '@/UI/Extra/extra.module.scss'; import { useValue } from '@/hooks/useValue'; @@ -7,6 +7,7 @@ import { setStage } from '@/store/stageReducer'; import { GoEnd, GoStart, MusicList, PlayOne, SquareSmall } from '@icon-park/react'; import useSoundEffect from '@/hooks/useSoundEffect'; import { setGuiAsset } from '@/store/GUIReducer'; +import bgmManager from '@/Core/Modules/audio/bgmManager'; export function ExtraBgm() { const { playSeClick, playSeEnter } = useSoundEffect(); @@ -88,8 +89,7 @@ export function ExtraBgm() {
{ playSeClick(); - const bgmControl: HTMLAudioElement = document.getElementById('currentBgm') as HTMLAudioElement; - bgmControl?.play().then(); + bgmManager.play({ src: currentBgmSrc, volume: 100, enter: 500, exit: 500 }); }} onMouseEnter={playSeEnter} className={styles.bgmControlButton} @@ -113,8 +113,7 @@ export function ExtraBgm() {
{ playSeClick(); - const bgmControl: HTMLAudioElement = document.getElementById('currentBgm') as HTMLAudioElement; - bgmControl.pause(); + bgmManager.stop(500); }} onMouseEnter={playSeEnter} className={styles.bgmControlButton} diff --git a/packages/webgal/src/UI/Menu/MenuPanel/MenuPanel.tsx b/packages/webgal/src/UI/Menu/MenuPanel/MenuPanel.tsx index 85e1100d6..ac76a275e 100644 --- a/packages/webgal/src/UI/Menu/MenuPanel/MenuPanel.tsx +++ b/packages/webgal/src/UI/Menu/MenuPanel/MenuPanel.tsx @@ -1,6 +1,5 @@ import styles from './menuPanel.module.scss'; import { MenuPanelButton } from './MenuPanelButton'; -import { playBgm } from '@/Core/controller/stage/playBgm'; import { MenuPanelTag } from '@/store/guiInterface'; import { useDispatch, useSelector } from 'react-redux'; import { RootState } from '@/store/store'; @@ -80,7 +79,7 @@ export const MenuPanel = () => { backToTitle(); dispatch(setVisibility({ component: 'showMenuPanel', visibility: false })); }, - rightFunc: () => {}, + rightFunc: () => { }, }); }} tagName={t('title.title')} diff --git a/packages/webgal/src/UI/Title/Title.tsx b/packages/webgal/src/UI/Title/Title.tsx index 6caf8c922..7cbd3ea4e 100644 --- a/packages/webgal/src/UI/Title/Title.tsx +++ b/packages/webgal/src/UI/Title/Title.tsx @@ -8,13 +8,14 @@ import useSoundEffect from '@/hooks/useSoundEffect'; import useApplyStyle from '@/hooks/useApplyStyle'; import { keyboard } from '@/hooks/useHotkey'; import useConfigData from '@/hooks/useConfigData'; -import { playBgm } from '@/Core/controller/stage/playBgm'; import { continueGame, startGame } from '@/Core/controller/gamePlay/startContinueGame'; import { showGlogalDialog } from '../GlobalDialog/GlobalDialog'; import styles from './title.module.scss'; +import bgmManager from '@/Core/Modules/audio/bgmManager'; /** 标题页 */ export default function Title() { + const stageStore = useSelector((webgalStore: RootState) => webgalStore.stage); const userDataState = useSelector((state: RootState) => state.userData); const GUIState = useSelector((state: RootState) => state.GUI); const dispatch = useDispatch(); @@ -44,7 +45,7 @@ export default function Title() {
{ - playBgm(GUIState.titleBgm); + bgmManager.play({ src: GUIState.titleBgm, volume: 100, enter: 2000 }); dispatch(setVisibility({ component: 'isEnterGame', visibility: true })); if (fullScreen === fullScreenOption.on) { document.documentElement.requestFullscreen(); @@ -132,7 +133,7 @@ export default function Title() { leftFunc: () => { window.close(); }, - rightFunc: () => {}, + rightFunc: () => { }, }); }} onMouseEnter={playSeEnter} diff --git a/packages/webgal/src/store/stageInterface.ts b/packages/webgal/src/store/stageInterface.ts index fd319d71f..7366edb65 100644 --- a/packages/webgal/src/store/stageInterface.ts +++ b/packages/webgal/src/store/stageInterface.ts @@ -225,8 +225,9 @@ export interface IStageState { bgm: { // 背景音乐 src: string; // 背景音乐 文件地址(相对或绝对) - enter: number; // 背景音乐 淡入或淡出的毫秒数 volume: number; // 背景音乐 音量调整(0 - 100) + enter: number; // 背景音乐 淡入的毫秒数 + exit: number; // 背景音乐 淡出的毫秒数 }; uiSe: string; // 用户界面音效 文件地址(相对或绝对) miniAvatar: string; // 小头像 文件地址(相对或绝对) diff --git a/packages/webgal/src/store/stageReducer.ts b/packages/webgal/src/store/stageReducer.ts index 57e1ffe42..90dee34ff 100644 --- a/packages/webgal/src/store/stageReducer.ts +++ b/packages/webgal/src/store/stageReducer.ts @@ -46,8 +46,9 @@ export const initState: IStageState = { bgm: { // 背景音乐 src: '', // 背景音乐 文件地址(相对或绝对) - enter: 0, // 背景音乐 淡入或淡出的毫秒数 volume: 100, // 背景音乐 音量调整(0 - 100) + enter: 0, // 背景音乐 淡入的毫秒数 + exit: 0, // 背景音乐 淡出的毫秒数 }, uiSe: '', // 用户界面音效 文件地址(相对或绝对) miniAvatar: '', // 小头像 文件地址(相对或绝对) diff --git a/yarn.lock b/yarn.lock index 9ef0a5ec1..9642feaa2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3488,6 +3488,16 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +gsap@^3.14.2: + version "3.14.2" + resolved "https://registry.yarnpkg.com/gsap/-/gsap-3.14.2.tgz#6a9ea31e5046948e0be61eae006ae576ca5937d6" + integrity sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA== + has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"