From 38c149be8650c1eeb50f319900c19d706bcec1be Mon Sep 17 00:00:00 2001 From: 22 <60903333+nini22P@users.noreply.github.com> Date: Thu, 18 Dec 2025 20:59:27 +0800 Subject: [PATCH 1/3] feat: on-demand rendering --- .../controller/stage/pixi/PixiController.ts | 86 ++++++++++++++++++- .../controller/storage/jumpFromBacklog.ts | 2 + 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts b/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts index 76ec7f3d6..7ae875463 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts @@ -88,12 +88,12 @@ export default class PixiStage { public frameDuration = 16.67; public notUpdateBacklogEffects = false; public readonly figureContainer: PIXI.Container; - public figureObjects: Array = []; + public figureObjects = this.createReactiveList([]); public stageWidth = SCREEN_CONSTANTS.width; public stageHeight = SCREEN_CONSTANTS.height; public assetLoader = new PIXI.Loader(); public readonly backgroundContainer: PIXI.Container; - public backgroundObjects: Array = []; + public backgroundObjects = this.createReactiveList([]); public mainStageObject: IStageObject; /** * 添加 Spine 立绘 @@ -104,11 +104,15 @@ export default class PixiStage { public addSpineFigure = addSpineFigureImpl.bind(this); public addSpineBg = addSpineBgImpl.bind(this); // 注册到 Ticker 上的函数 - private stageAnimations: Array = []; + private stageAnimations = this.createReactiveList([]); private loadQueue: { url: string; callback: () => void; name?: string }[] = []; private live2dFigureRecorder: Array = []; // 锁定变换对象(对象可能正在执行动画,不能应用变换) private lockTransformTarget: Array = []; + // 手动请求渲染防抖标记 + private isRenderPending = false; + // 更新 ticker 状态的防抖标记 + private isTickerUpdatePending = false; /** * 暂时没用上,以后可能用 @@ -121,6 +125,7 @@ export default class PixiStage { const app = new PIXI.Application({ backgroundAlpha: 0, preserveDrawingBuffer: true, + autoStart: false, }); // @ts-ignore @@ -194,7 +199,22 @@ export default class PixiStage { this.callLoader(); }; reload(); - this.initialize().then(() => {}); + this.initialize().then(() => { }); + this.requestRender(); + } + + public requestRender() { + if (this.isRenderPending) return; + this.isRenderPending = true; + + Promise.resolve().then(() => { + requestAnimationFrame(() => { + this.isRenderPending = false; + if (!this.currentApp?.ticker.started) { + this.currentApp?.render(); + } + }); + }); } public getFigureObjects() { @@ -346,6 +366,7 @@ export default class PixiStage { return; } sprite.texture = texture; + this.requestRender(); }); } @@ -374,6 +395,7 @@ export default class PixiStage { return; } sprite.texture = texture; + this.requestRender(); }); } @@ -436,6 +458,7 @@ export default class PixiStage { // 挂载 thisBgContainer.addChild(bgSprite); + this.requestRender(); } }, 0); }; @@ -610,6 +633,7 @@ export default class PixiStage { } thisFigureContainer.pivot.set(0, this.stageHeight / 2); thisFigureContainer.addChild(figureSprite); + this.requestRender(); } }, 0); }; @@ -1100,6 +1124,60 @@ export default class PixiStage { console.error('Failed to load figureCash:', error); } } + + private createReactiveList(array: T[]): T[] { + return new Proxy(array, { + // eslint-disable-next-line max-params + set: (target, property, value, receiver) => { + const result = Reflect.set(target, property, value, receiver); + if (property !== 'length') { + this.updateTickerStatus(); + } else { + this.updateTickerStatus(); + } + return result; + }, + deleteProperty: (target, property) => { + const result = Reflect.deleteProperty(target, property); + this.updateTickerStatus(); + return result; + }, + }); + } + + private updateTickerStatus() { + if (this.isTickerUpdatePending) return; + this.isTickerUpdatePending = true; + + Promise.resolve().then(() => { + this.isTickerUpdatePending = false; + const app = this.currentApp; + if (!app) return; + + const hasActiveAnimations = this.stageAnimations.length > 0; + const hasLive2D = this.figureObjects.some((fig) => fig.sourceType === 'live2d'); + const hasSpine = this.figureObjects.some((fig) => fig.sourceType === 'spine'); + const hasDynamicBg = this.backgroundObjects.some((bg) => bg.sourceType === 'video' || bg.sourceType === 'gif'); + const hasGifFigure = this.figureObjects.some((fig) => fig.sourceType === 'gif'); + + const shouldRun = hasActiveAnimations || hasLive2D || hasSpine || hasDynamicBg || hasGifFigure; + + if (shouldRun) { + if (!app.ticker.started) { + app.ticker.start(); + logger.debug('Ticker: STARTED'); + } + } else { + if (app.ticker.started) { + app.ticker.stop(); + this.currentApp?.render(); + logger.debug('Ticker: STOPPED'); + } else { + this.requestRender(); + } + } + }); + } } function updateCurrentBacklogEffects(newEffects: IEffect[]) { diff --git a/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts b/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts index 2ebf4ce6b..48098db7f 100644 --- a/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts +++ b/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts @@ -24,6 +24,8 @@ export const restorePerform = () => { performToRestore.forEach((e) => { runScript(e.script); }); + // 重新渲染 + WebGAL.gameplay.pixiStage?.requestRender(); }; /** From 546ddbb3d60fa4345bb71678ef76f1bfb476f6c4 Mon Sep 17 00:00:00 2001 From: 22 <60903333+nini22P@users.noreply.github.com> Date: Thu, 18 Dec 2025 21:06:15 +0800 Subject: [PATCH 2/3] simplify code for on-demand rendering --- .../controller/stage/pixi/PixiController.ts | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts b/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts index 7ae875463..92e455a15 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts @@ -207,13 +207,11 @@ export default class PixiStage { if (this.isRenderPending) return; this.isRenderPending = true; - Promise.resolve().then(() => { - requestAnimationFrame(() => { - this.isRenderPending = false; - if (!this.currentApp?.ticker.started) { - this.currentApp?.render(); - } - }); + requestAnimationFrame(() => { + this.isRenderPending = false; + if (!this.currentApp?.ticker.started) { + this.currentApp?.render(); + } }); } @@ -1130,11 +1128,7 @@ export default class PixiStage { // eslint-disable-next-line max-params set: (target, property, value, receiver) => { const result = Reflect.set(target, property, value, receiver); - if (property !== 'length') { - this.updateTickerStatus(); - } else { - this.updateTickerStatus(); - } + this.updateTickerStatus(); return result; }, deleteProperty: (target, property) => { @@ -1155,12 +1149,16 @@ export default class PixiStage { if (!app) return; const hasActiveAnimations = this.stageAnimations.length > 0; - const hasLive2D = this.figureObjects.some((fig) => fig.sourceType === 'live2d'); - const hasSpine = this.figureObjects.some((fig) => fig.sourceType === 'spine'); - const hasDynamicBg = this.backgroundObjects.some((bg) => bg.sourceType === 'video' || bg.sourceType === 'gif'); - const hasGifFigure = this.figureObjects.some((fig) => fig.sourceType === 'gif'); - - const shouldRun = hasActiveAnimations || hasLive2D || hasSpine || hasDynamicBg || hasGifFigure; + const allObjects = [...this.figureObjects, ...this.backgroundObjects]; + const hasDynamicObjects = allObjects.some( + (obj) => + obj.sourceType === 'live2d' || + obj.sourceType === 'spine' || + obj.sourceType === 'video' || + obj.sourceType === 'gif', + ); + + const shouldRun = hasActiveAnimations || hasDynamicObjects; if (shouldRun) { if (!app.ticker.started) { From 4f6fe37febec45e8a2164f24f9cdd98e4fce39d7 Mon Sep 17 00:00:00 2001 From: 22 <60903333+nini22P@users.noreply.github.com> Date: Sat, 20 Dec 2025 14:47:59 +0800 Subject: [PATCH 3/3] feat: get deltaMS from the ticker --- .../controller/stage/pixi/PixiController.ts | 53 +------------------ .../stage/pixi/animations/template.ts | 6 +-- .../stage/pixi/animations/testblur.ts | 4 +- .../stage/pixi/animations/universalSoftIn.ts | 4 +- .../stage/pixi/animations/universalSoftOff.ts | 4 +- .../controller/storage/jumpFromBacklog.ts | 5 +- 6 files changed, 13 insertions(+), 63 deletions(-) diff --git a/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts b/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts index 92e455a15..15924905c 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts @@ -85,7 +85,6 @@ export default class PixiStage { public readonly mainStageContainer: WebGALPixiContainer; public readonly foregroundEffectsContainer: PIXI.Container; public readonly backgroundEffectsContainer: PIXI.Container; - public frameDuration = 16.67; public notUpdateBacklogEffects = false; public readonly figureContainer: PIXI.Container; public figureObjects = this.createReactiveList([]); @@ -187,19 +186,13 @@ export default class PixiStage { this.backgroundContainer, ); this.currentApp = app; - // 每 5s 获取帧率,并且防 loader 死 - const update = () => { - this.updateFps(); - setTimeout(update, 10000); - }; - update(); // loader 防死 const reload = () => { setTimeout(reload, 500); this.callLoader(); }; reload(); - this.initialize().then(() => { }); + this.initialize(); this.requestRender(); } @@ -1097,13 +1090,6 @@ export default class PixiStage { } } - private updateFps() { - getScreenFps?.(120).then((fps) => { - this.frameDuration = 1000 / (fps as number); - // logger.info('当前帧率', fps); - }); - } - private lockStageObject(targetName: string) { this.lockTransformTarget.push(targetName); } @@ -1188,40 +1174,3 @@ function updateCurrentBacklogEffects(newEffects: IEffect[]) { webgalStore.dispatch(setStage({ key: 'effects', value: newEffects })); } - -/** - * @param {number} targetCount 不小于1的整数,表示经过targetCount帧之后返回结果 - * @return {Promise} - */ -const getScreenFps = (() => { - // 先做一下兼容性处理 - const nextFrame = [ - window.requestAnimationFrame, - // @ts-ignore - window.webkitRequestAnimationFrame, - // @ts-ignore - window.mozRequestAnimationFrame, - ].find((fn) => fn); - if (!nextFrame) { - console.error('requestAnimationFrame is not supported!'); - return; - } - return (targetCount = 60) => { - // 判断参数是否合规 - if (targetCount < 1) throw new Error('targetCount cannot be less than 1.'); - const beginDate = Date.now(); - let count = 0; - return new Promise((resolve) => { - (function log() { - nextFrame(() => { - if (++count >= targetCount) { - const diffDate = Date.now() - beginDate; - const fps = (count / diffDate) * 1000; - return resolve(fps); - } - log(); - }); - })(); - }); - }; -})(); diff --git a/packages/webgal/src/Core/controller/stage/pixi/animations/template.ts b/packages/webgal/src/Core/controller/stage/pixi/animations/template.ts index 74b017a10..9ae4c4c23 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/animations/template.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/animations/template.ts @@ -14,13 +14,13 @@ export function generateTemplateAnimationObj(targetKey: string, duration: number /** * 在此书写为动画设置初态的操作 */ - function setStartState() {} + function setStartState() { } // TODO:通用终态设置 /** * 在此书写为动画设置终态的操作 */ - function setEndState() {} + function setEndState() { } /** * 在此书写动画每一帧执行的函数 @@ -31,7 +31,7 @@ export function generateTemplateAnimationObj(targetKey: string, duration: number // 要操控的精灵 const sprite = target.pixiContainer; // 每一帧的时间 - const baseDuration = WebGAL.gameplay.pixiStage!.frameDuration; + const currentDeltaMS = WebGAL.gameplay.pixiStage!.currentApp!.ticker.deltaMS; /** * 在下面书写具体的动画 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 496e35fed..5d88127c5 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/animations/testblur.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/animations/testblur.ts @@ -36,8 +36,8 @@ export function generateTestblurAnimationObj(targetKey: string, duration: number function tickerFunc(delta: number) { if (target) { const container = target.pixiContainer; - const baseDuration = WebGAL.gameplay.pixiStage!.frameDuration; - const currentAddOplityDelta = (duration / baseDuration) * delta; + const currentDeltaMS = WebGAL.gameplay.pixiStage!.currentApp!.ticker.deltaMS; + const currentAddOplityDelta = (duration / currentDeltaMS) * delta; const increasement = 1 / currentAddOplityDelta; const decreasement = 5 / currentAddOplityDelta; if (container) 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 61f8a3d69..02108bbaf 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftIn.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftIn.ts @@ -35,9 +35,9 @@ export function generateUniversalSoftInAnimationObj(targetKey: string, duration: function tickerFunc(delta: number) { if (target) { const sprite = target.pixiContainer; - const baseDuration = WebGAL.gameplay.pixiStage!.frameDuration; + const currentDeltaMS = WebGAL.gameplay.pixiStage!.currentApp!.ticker.deltaMS; - elapsedTime += baseDuration; + elapsedTime += currentDeltaMS; const realElapsedTime = Math.min(elapsedTime, duration); const progress = realElapsedTime / duration; 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 3f5477457..773536310 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftOff.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/animations/universalSoftOff.ts @@ -35,9 +35,9 @@ export function generateUniversalSoftOffAnimationObj(targetKey: string, duration function tickerFunc(delta: number) { if (target) { const targetContainer = target.pixiContainer; - const baseDuration = WebGAL.gameplay.pixiStage!.frameDuration; + const currentDeltaMS = WebGAL.gameplay.pixiStage!.currentApp!.ticker.deltaMS; - elapsedTime += baseDuration; + elapsedTime += currentDeltaMS; const realElapsedTime = Math.min(elapsedTime, duration); const progress = realElapsedTime / duration; diff --git a/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts b/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts index 48098db7f..b8e50caa1 100644 --- a/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts +++ b/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts @@ -24,8 +24,6 @@ export const restorePerform = () => { performToRestore.forEach((e) => { runScript(e.script); }); - // 重新渲染 - WebGAL.gameplay.pixiStage?.requestRender(); }; /** @@ -79,4 +77,7 @@ export const jumpFromBacklog = (index: number, refetchScene = true) => { // 重新显示 TextBox dispatch(setVisibility({ component: 'showTextBox', visibility: true })); + + // 重新渲染 + setTimeout(() => WebGAL.gameplay.pixiStage?.requestRender(), 100); };