From 3b417d29781d24eb74db20ca8e4496e03034e400 Mon Sep 17 00:00:00 2001 From: Hardy--Lee <> Date: Wed, 24 Sep 2025 22:38:31 +0800 Subject: [PATCH] fix: clear animation before exiting --- .../controller/stage/pixi/PixiController.ts | 32 +++++++---- .../stage/pixi/animations/testblur.ts | 11 ++-- .../stage/pixi/animations/timeline.ts | 33 +++++------ .../stage/pixi/animations/universalSoftIn.ts | 6 +- .../stage/pixi/animations/universalSoftOff.ts | 6 +- .../src/Core/gameScripts/changeBg/index.ts | 15 +++-- .../src/Core/gameScripts/changeFigure.ts | 55 +++++++++++-------- .../src/Stage/MainStage/useSetFigure.ts | 2 +- 8 files changed, 92 insertions(+), 68 deletions(-) diff --git a/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts b/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts index 546914be4..35680f96c 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts @@ -34,7 +34,7 @@ export interface IStageObject { uuid: string; // 一般与作用目标有关 key: string; - pixiContainer: WebGALPixiContainer; + pixiContainer: WebGALPixiContainer | null; // 相关的源 url sourceUrl: string; sourceExt: string; @@ -235,7 +235,7 @@ export default class PixiStage { const targetPixiContainer = this.getStageObjByKey(target); if (targetPixiContainer) { const container = targetPixiContainer.pixiContainer; - PixiStage.assignTransform(container, effect.transform); + if (container) PixiStage.assignTransform(container, effect.transform); } return; } @@ -776,6 +776,7 @@ export default class PixiStage { const figureRecordTarget = this.live2dFigureRecorder.find((e) => e.target === key); if (target && figureRecordTarget?.motion !== motion) { const container = target.pixiContainer; + if (!container) return; const children = container.children; for (const model of children) { let category_name = motion; @@ -799,6 +800,7 @@ export default class PixiStage { if (target?.sourceType !== 'spine') return; const container = target.pixiContainer; + if (!container) return; // Spine figure 结构: Container -> Sprite -> Spine const sprite = container.children[0] as PIXI.Container; if (sprite?.children?.[0]) { @@ -825,6 +827,7 @@ export default class PixiStage { const figureRecordTarget = this.live2dFigureRecorder.find((e) => e.target === key); if (target && figureRecordTarget?.expression !== expression) { const container = target.pixiContainer; + if (!container) return; const children = container.children; for (const model of children) { // @ts-ignore @@ -840,6 +843,7 @@ export default class PixiStage { const figureRecordTarget = this.live2dFigureRecorder.find((e) => e.target === key); if (target && !isEqual(figureRecordTarget?.blink, blinkParam)) { const container = target.pixiContainer; + if (!container) return; const children = container.children; let newBlinkParam: BlinkParam = { ...baseBlinkParam, ...blinkParam }; // 继承现有 BlinkParam @@ -860,6 +864,7 @@ export default class PixiStage { const figureRecordTarget = this.live2dFigureRecorder.find((e) => e.target === key); if (target && !isEqual(figureRecordTarget?.focus, focusParam)) { const container = target.pixiContainer; + if (!container) return; const children = container.children; let newFocusParam: FocusParam = { ...baseFocusParam, ...focusParam }; // 继承现有 FocusParam @@ -883,6 +888,7 @@ export default class PixiStage { const target = this.figureObjects.find((e) => e.key === key); if (target && target.sourceType === 'live2d') { const container = target.pixiContainer; + if (!container) return; const children = container.children; for (const model of children) { // @ts-ignore @@ -925,20 +931,26 @@ export default class PixiStage { const indexBg = this.backgroundObjects.findIndex((e) => e.key === key); if (indexFig >= 0) { const bgSprite = this.figureObjects[indexFig]; - for (const element of bgSprite.pixiContainer.children) { - element.destroy(); + if (bgSprite.pixiContainer) { + for (const element of bgSprite.pixiContainer.children) { + element.destroy(); + } + bgSprite.pixiContainer.destroy(); + this.figureContainer.removeChild(bgSprite.pixiContainer); } - bgSprite.pixiContainer.destroy(); - this.figureContainer.removeChild(bgSprite.pixiContainer); + bgSprite.pixiContainer = null; this.figureObjects.splice(indexFig, 1); } if (indexBg >= 0) { const bgSprite = this.backgroundObjects[indexBg]; - for (const element of bgSprite.pixiContainer.children) { - element.destroy(); + if (bgSprite.pixiContainer) { + for (const element of bgSprite.pixiContainer.children) { + element.destroy(); + } + bgSprite.pixiContainer.destroy(); + this.backgroundContainer.removeChild(bgSprite.pixiContainer); } - bgSprite.pixiContainer.destroy(); - this.backgroundContainer.removeChild(bgSprite.pixiContainer); + bgSprite.pixiContainer = null; this.backgroundObjects.splice(indexBg, 1); } // /** diff --git a/packages/webgal/src/Core/controller/stage/pixi/animations/testblur.ts b/packages/webgal/src/Core/controller/stage/pixi/animations/testblur.ts index 2bb9a98e9..496e35fed 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/animations/testblur.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/animations/testblur.ts @@ -10,7 +10,7 @@ export function generateTestblurAnimationObj(targetKey: string, duration: number * 在此书写为动画设置初态的操作 */ function setStartState() { - if (target) { + if (target?.pixiContainer) { target.pixiContainer.alpha = 0; // @ts-ignore target.pixiContainer.blur = 0; @@ -22,7 +22,7 @@ export function generateTestblurAnimationObj(targetKey: string, duration: number * 在此书写为动画设置终态的操作 */ function setEndState() { - if (target) { + if (target?.pixiContainer) { target.pixiContainer.alpha = 1; // @ts-ignore target.pixiContainer.blur = 5; @@ -40,9 +40,10 @@ export function generateTestblurAnimationObj(targetKey: string, duration: number const currentAddOplityDelta = (duration / baseDuration) * delta; const increasement = 1 / currentAddOplityDelta; const decreasement = 5 / currentAddOplityDelta; - if (container.alpha < 1) { - container.alpha += increasement; - } + if (container) + if (container.alpha < 1) { + container.alpha += increasement; + } // @ts-ignore if (container.blur < 5) { // @ts-ignore diff --git a/packages/webgal/src/Core/controller/stage/pixi/animations/timeline.ts b/packages/webgal/src/Core/controller/stage/pixi/animations/timeline.ts index 24f7dbc7e..20d8838b5 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/animations/timeline.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/animations/timeline.ts @@ -42,7 +42,6 @@ export function generateTimelineObj( times.push(currentDelay / duration); } else times.push(0); } - const container = target?.pixiContainer; let animateInstance: ReturnType | null = null; // 只有有 duration 的时候才有动画 if (duration > 0) { @@ -52,13 +51,13 @@ export function generateTimelineObj( duration, ease: easeArray, onUpdate: (updateValue) => { - if (container) { + if (target?.pixiContainer) { const { scaleX, scaleY, ...val } = updateValue; // @ts-ignore - PixiStage.assignTransform(container, omitBy(val, isUndefined)); + PixiStage.assignTransform(target.pixiContainer, omitBy(val, isUndefined)); // 因为 popmotion 不能用嵌套,scale 要手动设置 - if (!isUndefined(scaleX)) container.scale.x = scaleX; - if (!isUndefined(scaleY)) container.scale.y = scaleY; + if (!isUndefined(scaleX)) target.pixiContainer.scale.x = scaleX; + if (!isUndefined(scaleY)) target.pixiContainer.scale.y = scaleY; } }, }); @@ -77,13 +76,11 @@ export function generateTimelineObj( const assignValue = omitBy({ x: position.x, y: position.y, ...state }, isUndefined); // @ts-ignore PixiStage.assignTransform(target?.pixiContainer, assignValue); - if (target?.pixiContainer) { - if (!isUndefined(scale.x)) { - target.pixiContainer.scale.x = scale.x; - } - if (!isUndefined(scale?.y)) { - target.pixiContainer.scale.y = scale.y; - } + if (!isUndefined(scale.x)) { + target.pixiContainer.scale.x = scale.x; + } + if (!isUndefined(scale?.y)) { + target.pixiContainer.scale.y = scale.y; } } } @@ -101,13 +98,11 @@ export function generateTimelineObj( const assignValue = omitBy({ x: position.x, y: position.y, ...state }, isUndefined); // @ts-ignore PixiStage.assignTransform(target?.pixiContainer, assignValue); - if (target?.pixiContainer) { - if (!isUndefined(scale.x)) { - target.pixiContainer.scale.x = scale.x; - } - if (!isUndefined(scale?.y)) { - target.pixiContainer.scale.y = scale.y; - } + if (!isUndefined(scale.x)) { + target.pixiContainer.scale.x = scale.x; + } + if (!isUndefined(scale?.y)) { + target.pixiContainer.scale.y = scale.y; } } } diff --git a/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftIn.ts b/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftIn.ts index 61bbd2707..61f8a3d69 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftIn.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftIn.ts @@ -12,7 +12,7 @@ export function generateUniversalSoftInAnimationObj(targetKey: string, duration: */ function setStartState() { elapsedTime = 0; // Reset timer when animation starts - if (target) { + if (target?.pixiContainer) { // 修正:不再强制设为 0,而是记录当前的透明度 startAlpha = target.pixiContainer.alpha; } @@ -22,7 +22,7 @@ export function generateUniversalSoftInAnimationObj(targetKey: string, duration: * 在此书写为动画设置终态的操作 */ function setEndState() { - if (target) { + if (target?.pixiContainer) { // 终态是完全不透明,这保持不变 target.pixiContainer.alpha = 1; } @@ -49,7 +49,7 @@ export function generateUniversalSoftInAnimationObj(targetKey: string, duration: // 公式:最终值 = 初始值 + (目标值 - 初始值) * 进度 // 在这里,目标值是 1,所以公式为: // alpha = startAlpha + (1 - startAlpha) * easedProgress - sprite.alpha = startAlpha + (1 - startAlpha) * easedProgress; + if (sprite) sprite.alpha = startAlpha + (1 - startAlpha) * easedProgress; } } diff --git a/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftOff.ts b/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftOff.ts index 7c139c1a3..3f5477457 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftOff.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftOff.ts @@ -12,7 +12,7 @@ export function generateUniversalSoftOffAnimationObj(targetKey: string, duration */ function setStartState() { elapsedTime = 0; // 重置计时器 - if (target) { + if (target?.pixiContainer) { // 修正:不再强制设为1,而是记录当前的透明度 startAlpha = target.pixiContainer.alpha; } @@ -22,7 +22,7 @@ export function generateUniversalSoftOffAnimationObj(targetKey: string, duration * 在此书写为动画设置终态的操作 */ function setEndState() { - if (target) { + if (target?.pixiContainer) { // 终态是完全透明,这保持不变 target.pixiContainer.alpha = 0; } @@ -50,7 +50,7 @@ export function generateUniversalSoftOffAnimationObj(targetKey: string, duration // 在这里,目标值是 0,所以公式简化为: // alpha = startAlpha + (0 - startAlpha) * easedProgress // alpha = startAlpha * (1 - easedProgress) - targetContainer.alpha = startAlpha * (1 - easedProgress); + if (targetContainer) targetContainer.alpha = startAlpha * (1 - easedProgress); } } diff --git a/packages/webgal/src/Core/gameScripts/changeBg/index.ts b/packages/webgal/src/Core/gameScripts/changeBg/index.ts index d6f4a640a..a79eb4fbb 100644 --- a/packages/webgal/src/Core/gameScripts/changeBg/index.ts +++ b/packages/webgal/src/Core/gameScripts/changeBg/index.ts @@ -32,11 +32,18 @@ export const changeBg = (sentence: ISentence): IPerform => { dispatch(unlockCgInUserData({ name: unlockName, url, series })); } - /** - * 删掉相关 Effects,因为已经移除了 - */ - if (webgalStore.getState().stage.bgName !== sentence.content) { + // 检测 url 是否变化 + let isUrlChanged = webgalStore.getState().stage.bgName !== sentence.content; + if (isUrlChanged) { + // 清除动画 + WebGAL.gameplay.pixiStage?.removeAnimation('bg-main'); + // 移除旧的 effect dispatch(stageActions.removeEffectByTargetId(`bg-main`)); + // 标记旧的背景为退出中,防止旧背景继承新参数 + const oldStageObject = WebGAL.gameplay.pixiStage?.getStageObjByKey('bg-main'); + if (oldStageObject) { + oldStageObject.isExiting = true; + } } // 处理 transform 和 默认 transform diff --git a/packages/webgal/src/Core/gameScripts/changeFigure.ts b/packages/webgal/src/Core/gameScripts/changeFigure.ts index e64cbb016..f5131793b 100644 --- a/packages/webgal/src/Core/gameScripts/changeFigure.ts +++ b/packages/webgal/src/Core/gameScripts/changeFigure.ts @@ -12,7 +12,7 @@ import { logger } from '@/Core/util/logger'; import { getAnimateDuration } from '@/Core/Modules/animationFunctions'; import { WebGAL } from '@/Core/WebGAL'; import { baseBlinkParam, baseFocusParam, BlinkParam, FocusParam } from '@/Core/live2DCore'; -import { WEBGAL_NONE } from '../constants'; +import { STAGE_KEYS, WEBGAL_NONE } from '../constants'; /** * 更改立绘 * @param sentence 语句 @@ -46,9 +46,24 @@ export function changeFigure(sentence: ISentence): IPerform { } // id 与 自由立绘 - let key = getStringArgByKey(sentence, 'id') ?? ''; - const isFreeFigure = key ? true : false; - const id = key ? key : `fig-${pos}`; + let idFromArgs = getStringArgByKey(sentence, 'id') ?? ''; + const isFreeFigure = idFromArgs ? true : false; + let key = ''; + if (isFreeFigure) { + key = idFromArgs; + } else { + switch (pos) { + case 'center': + key = STAGE_KEYS.FIG_C; + break; + case 'left': + key = STAGE_KEYS.FIG_L; + break; + case 'right': + key = STAGE_KEYS.FIG_R; + break; + } + } // live2d 或 spine 相关 let motion = getStringArgByKey(sentence, 'motion') ?? ''; @@ -95,9 +110,9 @@ export function changeFigure(sentence: ISentence): IPerform { const dispatch = webgalStore.dispatch; const currentFigureAssociatedAnimation = webgalStore.getState().stage.figureAssociatedAnimation; - const filteredFigureAssociatedAnimation = currentFigureAssociatedAnimation.filter((item) => item.targetId !== id); + const filteredFigureAssociatedAnimation = currentFigureAssociatedAnimation.filter((item) => item.targetId !== key); const newFigureAssociatedAnimationItem = { - targetId: id, + targetId: key, animationFlag: animationFlag, mouthAnimation: { open: mouthOpen, @@ -112,9 +127,7 @@ export function changeFigure(sentence: ISentence): IPerform { filteredFigureAssociatedAnimation.push(newFigureAssociatedAnimationItem); dispatch(setStage({ key: 'figureAssociatedAnimation', value: filteredFigureAssociatedAnimation })); - /** - * 如果 url 没变,不移除 - */ + // 检测 url 是否变化 let isUrlChanged = true; if (key !== '') { const figWithKey = webgalStore.getState().stage.freeFigure.find((e) => e.key === key); @@ -140,12 +153,14 @@ export function changeFigure(sentence: ISentence): IPerform { } } } - /** - * 处理 Effects - */ + if (isUrlChanged) { - webgalStore.dispatch(stageActions.removeEffectByTargetId(id)); - const oldStageObject = WebGAL.gameplay.pixiStage?.getStageObjByKey(id); + // 清除动画 + WebGAL.gameplay.pixiStage?.removeAnimation(key); + // 移除旧的 effect + webgalStore.dispatch(stageActions.removeEffectByTargetId(key)); + // 标记旧的立绘为退出中,防止旧立绘继承新参数 + const oldStageObject = WebGAL.gameplay.pixiStage?.getStageObjByKey(key); if (oldStageObject) { oldStageObject.isExiting = true; } @@ -196,7 +211,7 @@ export function changeFigure(sentence: ISentence): IPerform { } }; - function setFigureData() { + function postFigureStateSet() { if (isUrlChanged) { // 当 url 发生变化时,即发生新立绘替换 // 应当赋予一些参数以默认值,防止从旧立绘的状态获取数据 @@ -236,26 +251,20 @@ export function changeFigure(sentence: ISentence): IPerform { */ const freeFigureItem: IFreeFigure = { key, name: content, basePosition: pos }; setAnimationNames(key, sentence); - setFigureData(); + postFigureStateSet(); dispatch(stageActions.setFreeFigureByKey(freeFigureItem)); } else { /** * 下面的代码是设置与位置关联的立绘的 */ - const positionMap = { - center: 'fig-center', - left: 'fig-left', - right: 'fig-right', - }; const dispatchMap: Record = { center: 'figName', left: 'figNameLeft', right: 'figNameRight', }; - key = positionMap[pos]; setAnimationNames(key, sentence); - setFigureData(); + postFigureStateSet(); dispatch(setStage({ key: dispatchMap[pos], value: content })); } diff --git a/packages/webgal/src/Stage/MainStage/useSetFigure.ts b/packages/webgal/src/Stage/MainStage/useSetFigure.ts index 0b07cde39..951bee319 100644 --- a/packages/webgal/src/Stage/MainStage/useSetFigure.ts +++ b/packages/webgal/src/Stage/MainStage/useSetFigure.ts @@ -63,7 +63,7 @@ export function useSetFigure(stageState: IStageState) { useEffect(() => { Object.entries(figureMetaData).forEach(([key, value]) => { const figureObject = WebGAL.gameplay.pixiStage?.getStageObjByKey(key); - if (figureObject && !figureObject.isExiting && value?.zIndex !== undefined) { + if (figureObject && !figureObject.isExiting && value?.zIndex !== undefined && figureObject.pixiContainer) { figureObject.pixiContainer.zIndex = value.zIndex; } });